diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 102acee..4a46a7a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,12 +1,6 @@ name: ci -on: - push: - branches: - - main - pull_request: - branches: - - main +on: [push, pull_request] jobs: fmt: diff --git a/Cargo.lock b/Cargo.lock index 7680e8f..76b0d5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,11 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "ahash" version = "0.4.7" @@ -32,6 +38,15 @@ version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afddf7f520a80dbf76e6f50a35bca42a2331ef227a28b3b6dc5c2e2338d114b1" +[[package]] +name = "arrayvec" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9" +dependencies = [ + "nodrop", +] + [[package]] name = "arrayvec" version = "0.5.2" @@ -70,12 +85,39 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "base64" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" +dependencies = [ + "byteorder", +] + [[package]] name = "base64" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "bigdecimal" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1374191e2dd25f9ae02e3aa95041ed5d747fc77b3c102b49fe2dd9a8117a6244" +dependencies = [ + "num-bigint 0.2.6", + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.2.1" @@ -94,13 +136,34 @@ dependencies = [ "wyz", ] +[[package]] +name = "block-buffer" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" +dependencies = [ + "block-padding", + "byte-tools", + "byteorder", + "generic-array 0.12.4", +] + [[package]] name = "block-buffer" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "generic-array", + "generic-array 0.14.4", +] + +[[package]] +name = "block-padding" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" +dependencies = [ + "byte-tools", ] [[package]] @@ -109,7 +172,7 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "699194c00f3a2effd3358d47f880646818e3d483190b17ebcdf598c654fb77e9" dependencies = [ - "base64", + "base64 0.13.0", "bollard-stubs", "bytes", "chrono", @@ -130,7 +193,7 @@ dependencies = [ "thiserror", "tokio", "tokio-util", - "url", + "url 2.2.1", "winapi", ] @@ -145,6 +208,12 @@ dependencies = [ "serde_with", ] +[[package]] +name = "bufstream" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8" + [[package]] name = "build_const" version = "0.2.1" @@ -157,6 +226,12 @@ version = "3.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe" +[[package]] +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" + [[package]] name = "byteorder" version = "1.4.2" @@ -177,16 +252,25 @@ dependencies = [ "async-trait", "bollard", "chrono", + "diesel", "dotenv", "futures", "hyper", + "lazy-init", + "lazy_static", "once_cell", + "r2d2", + "r2d2_mysql", "rand 0.8.3", + "regex", "reqwest", "serde", "serde_json", "sqlx", + "strum", + "strum_macros", "tokio", + "uuid 0.8.2", ] [[package]] @@ -195,6 +279,12 @@ version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + [[package]] name = "cfg-if" version = "1.0.0" @@ -246,13 +336,22 @@ dependencies = [ "build_const", ] +[[package]] +name = "crc32fast" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" +dependencies = [ + "cfg-if 1.0.0", +] + [[package]] name = "crossbeam-channel" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dca26ee1f8d361640700bde38b2c37d8c22b3ce2d360e1fc1c74ea4b0aa7d775" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "crossbeam-utils", ] @@ -262,7 +361,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f6cb3c7f5b8e51bc3ebb73a2327ad4abdbd119dc13223f14f961d2f38486756" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "crossbeam-utils", ] @@ -273,7 +372,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7e9d99fa91428effe99c5c6d4634cdeba32b8cf784fc428a2a687f61a952c49" dependencies = [ "autocfg 1.0.1", - "cfg-if", + "cfg-if 1.0.0", "lazy_static", ] @@ -321,13 +420,47 @@ dependencies = [ "syn", ] +[[package]] +name = "diesel" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "047bfc4d5c3bd2ef6ca6f981941046113524b9a9f9a7cbdfdd7ff40f58e6f542" +dependencies = [ + "byteorder", + "chrono", + "diesel_derives", + "mysqlclient-sys", + "r2d2", + "url 1.7.2", +] + +[[package]] +name = "diesel_derives" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45f5098f628d02a7a0f68ddba586fb61e80edec3bdc1be3b921f4ceec60858d3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +dependencies = [ + "generic-array 0.12.4", +] + [[package]] name = "digest" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array", + "generic-array 0.14.4", ] [[package]] @@ -336,7 +469,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "dirs-sys-next", ] @@ -372,7 +505,25 @@ version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", +] + +[[package]] +name = "fake-simd" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" + +[[package]] +name = "flate2" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3aec53de10fe96d7d8c565eb17f2c687bb5518a2ec453b5b1252964526abe0" +dependencies = [ + "cfg-if 1.0.0", + "crc32fast", + "libc", + "miniz_oxide", ] [[package]] @@ -403,7 +554,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" dependencies = [ "matches", - "percent-encoding", + "percent-encoding 2.1.0", ] [[package]] @@ -504,6 +655,15 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + [[package]] name = "generic-array" version = "0.14.4" @@ -520,7 +680,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "libc", "wasi 0.9.0+wasi-snapshot-preview1", ] @@ -531,7 +691,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "libc", "wasi 0.10.2+wasi-snapshot-preview1", ] @@ -686,6 +846,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "idna" version = "0.2.2" @@ -713,7 +884,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -737,6 +908,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy-init" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e71f2233af239a915476da8ee21a57331d82b9880c78220451ece7cb5862d313" + [[package]] name = "lazy_static" version = "1.4.0" @@ -746,17 +923,41 @@ dependencies = [ "spin", ] +[[package]] +name = "lexical" +version = "2.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9d189f82a78aa5c06e64f60cbd6f9ec37fe1cdb453dfd88ac2128c81157dc6c" +dependencies = [ + "cfg-if 0.1.10", + "lexical-core 0.4.8", + "rustc_version", +] + +[[package]] +name = "lexical-core" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34449d00c5d4066537f4dc72320b18e3aa421e8e92669250eecd664c618fefce" +dependencies = [ + "arrayvec 0.4.12", + "cfg-if 0.1.10", + "rustc_version", + "ryu", + "static_assertions 0.3.4", +] + [[package]] name = "lexical-core" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21f866863575d0e1d654fbeeabdc927292fdf862873dc3c96c6f753357e13374" dependencies = [ - "arrayvec", + "arrayvec 0.5.2", "bitflags", - "cfg-if", + "cfg-if 1.0.0", "ryu", - "static_assertions", + "static_assertions 1.1.0", ] [[package]] @@ -786,7 +987,7 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -813,6 +1014,16 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg 1.0.1", +] + [[package]] name = "mio" version = "0.7.9" @@ -836,6 +1047,78 @@ dependencies = [ "winapi", ] +[[package]] +name = "mysql" +version = "16.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dcfadc226d508e428e0c14b836844a82c6190cad563d07f29a19e80a469b6d7" +dependencies = [ + "bit-vec", + "bufstream", + "byteorder", + "flate2", + "fnv", + "libc", + "mysql_common", + "named_pipe", + "net2", + "nix", + "percent-encoding 2.1.0", + "regex", + "serde", + "serde_json", + "twox-hash", + "url 2.2.1", + "winapi", +] + +[[package]] +name = "mysql_common" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23a0a0c5627c3f30e54032689c82100f082e2190574832201735db1d40a9fe0f" +dependencies = [ + "base64 0.10.1", + "bigdecimal", + "bit-vec", + "bitflags", + "byteorder", + "chrono", + "lazy_static", + "lexical", + "num-bigint 0.2.6", + "num-traits", + "rand 0.7.3", + "regex", + "rust_decimal", + "serde", + "serde_json", + "sha1", + "sha2 0.8.2", + "time", + "twox-hash", + "uuid 0.7.4", +] + +[[package]] +name = "mysqlclient-sys" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e9637d93448044078aaafea7419aed69d301b4a12bcc4aa0ae856eb169bef85" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "named_pipe" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad9c443cce91fc3e12f017290db75dde490d685cdaaf508d7159d7cf41f0eb2b" +dependencies = [ + "winapi", +] + [[package]] name = "native-tls" version = "0.2.7" @@ -854,6 +1137,36 @@ dependencies = [ "tempfile", ] +[[package]] +name = "net2" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "winapi", +] + +[[package]] +name = "nix" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2e0b4f3320ed72aaedb9a5ac838690a8047c7b275da22711fddff4f8a14229" +dependencies = [ + "bitflags", + "cc", + "cfg-if 0.1.10", + "libc", + "void", +] + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + [[package]] name = "nom" version = "6.1.2" @@ -862,7 +1175,7 @@ checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2" dependencies = [ "bitvec", "funty", - "lexical-core", + "lexical-core 0.7.5", "memchr", "version_check", ] @@ -963,6 +1276,12 @@ version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" +[[package]] +name = "opaque-debug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" + [[package]] name = "opaque-debug" version = "0.3.0" @@ -976,7 +1295,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "038d43985d1ddca7a9900630d8cd031b56e4794eecc2e9ea39dd17aa04399a70" dependencies = [ "bitflags", - "cfg-if", + "cfg-if 1.0.0", "foreign-types", "lazy_static", "libc", @@ -1019,7 +1338,7 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "instant", "libc", "redox_syscall", @@ -1033,11 +1352,17 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd56cbd21fea48d0c440b41cd69c589faacade08c992d9a54e471b79d0fd13eb" dependencies = [ - "base64", + "base64 0.13.0", "once_cell", "regex", ] +[[package]] +name = "percent-encoding" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" + [[package]] name = "percent-encoding" version = "2.1.0" @@ -1118,6 +1443,28 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r2d2" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "545c5bc2b880973c9c10e4067418407a0ccaa3091781d1671d46eb35107cb26f" +dependencies = [ + "log", + "parking_lot", + "scheduled-thread-pool", +] + +[[package]] +name = "r2d2_mysql" +version = "16.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f74d9d1e2987980d30a8b76a14629fc7cc96ce9d34bb35b08842af07c3c5b414" +dependencies = [ + "mysql", + "r2d2", + "rustc-serialize", +] + [[package]] name = "radium" version = "0.5.3" @@ -1257,7 +1604,7 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0460542b551950620a3648c6aa23318ac6b3cd779114bd873209e6e8b5eb1c34" dependencies = [ - "base64", + "base64 0.13.0", "bytes", "encoding_rs", "futures-core", @@ -1272,14 +1619,14 @@ dependencies = [ "log", "mime", "native-tls", - "percent-encoding", + "percent-encoding 2.1.0", "pin-project-lite", "serde", "serde_json", "serde_urlencoded", "tokio", "tokio-native-tls", - "url", + "url 2.2.1", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -1308,7 +1655,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3648b669b10afeab18972c105e284a7b953a669b0be3514c27f9b17acab2f9cd" dependencies = [ "byteorder", - "digest", + "digest 0.9.0", "lazy_static", "num-bigint-dig", "num-integer", @@ -1316,20 +1663,46 @@ dependencies = [ "num-traits", "pem", "rand 0.7.3", - "sha2", + "sha2 0.9.3", "simple_asn1", "subtle", "thiserror", "zeroize", ] +[[package]] +name = "rust_decimal" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72e80c4f9a71b5949e283189c3c3ae35fedddecfe112e75c9d58751c36605b62" +dependencies = [ + "arrayvec 0.5.2", + "num-traits", + "serde", +] + +[[package]] +name = "rustc-serialize" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver", +] + [[package]] name = "rustls" version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "064fd21ff87c6e87ed4506e68beb42459caa4a0e2eb144932e6776768556980b" dependencies = [ - "base64", + "base64 0.13.0", "log", "ring", "sct", @@ -1352,6 +1725,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "scheduled-thread-pool" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6f74fd1204073fa02d5d5d68bec8021be4c38690b61264b2fdb48083d0e7d7" +dependencies = [ + "parking_lot", +] + [[package]] name = "scopeguard" version = "1.1.0" @@ -1391,6 +1773,21 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + [[package]] name = "serde" version = "1.0.124" @@ -1463,11 +1860,29 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfebf75d25bd900fd1e7d11501efab59bc846dbc76196839663e6637bba9f25f" dependencies = [ - "block-buffer", - "cfg-if", + "block-buffer 0.9.0", + "cfg-if 1.0.0", "cpuid-bool", - "digest", - "opaque-debug", + "digest 0.9.0", + "opaque-debug 0.3.0", +] + +[[package]] +name = "sha1" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" + +[[package]] +name = "sha2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a256f46ea78a0c0d9ff00077504903ac881a1dafdc20da66545699e7776b3e69" +dependencies = [ + "block-buffer 0.7.3", + "digest 0.8.1", + "fake-simd", + "opaque-debug 0.2.3", ] [[package]] @@ -1476,11 +1891,11 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa827a14b29ab7f44778d14a88d3cb76e949c45083f7dbfa507d0cb699dc12de" dependencies = [ - "block-buffer", - "cfg-if", + "block-buffer 0.9.0", + "cfg-if 1.0.0", "cpuid-bool", - "digest", - "opaque-debug", + "digest 0.9.0", + "opaque-debug 0.3.0", ] [[package]] @@ -1521,7 +1936,7 @@ version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "libc", "winapi", ] @@ -1563,7 +1978,7 @@ checksum = "b1cad9cae4ca8947eba1a90e8ec7d3c59e7a768e2f120dc9013b669c34a90711" dependencies = [ "ahash 0.6.3", "atoi", - "base64", + "base64 0.13.0", "bitflags", "byteorder", "bytes", @@ -1572,12 +1987,12 @@ dependencies = [ "crossbeam-channel", "crossbeam-queue", "crossbeam-utils", - "digest", + "digest 0.9.0", "either", "futures-channel", "futures-core", "futures-util", - "generic-array", + "generic-array 0.14.4", "hashlink", "hex", "itoa", @@ -1587,20 +2002,20 @@ dependencies = [ "num-bigint 0.3.2", "once_cell", "parking_lot", - "percent-encoding", + "percent-encoding 2.1.0", "rand 0.7.3", "rsa", "rustls", "serde", "sha-1", - "sha2", + "sha2 0.9.3", "smallvec", "sqlformat", "sqlx-rt", "stringprep", "thiserror", "tokio-stream", - "url", + "url 2.2.1", "webpki", "webpki-roots", "whoami", @@ -1622,11 +2037,11 @@ dependencies = [ "quote", "serde", "serde_json", - "sha2", + "sha2 0.9.3", "sqlx-core", "sqlx-rt", "syn", - "url", + "url 2.2.1", ] [[package]] @@ -1640,6 +2055,12 @@ dependencies = [ "tokio-rustls", ] +[[package]] +name = "static_assertions" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f3eb36b47e512f8f1c9e3d10c2c1965bc992bd9cdb024fa581e2194501c83d3" + [[package]] name = "static_assertions" version = "1.1.0" @@ -1662,6 +2083,27 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strum" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7318c509b5ba57f18533982607f24070a55d353e90d4cae30c467cdb2ad5ac5c" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8bc6b87a5112aeeab1f4a9f7ab634fe6cbefc4850006df31267f4cfb9e3149" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "subtle" version = "2.4.0" @@ -1703,7 +2145,7 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "libc", "rand 0.8.3", "redox_syscall", @@ -1854,7 +2296,7 @@ version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01ebdc2bb4498ab1ab5f5b73c5803825e60199229ccba0698170e3be0e7f959f" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "pin-project-lite", "tracing-core", ] @@ -1874,6 +2316,17 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +[[package]] +name = "twox-hash" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f8ab788026715fa63b31960869617cba39117e520eb415b0139543e325ab59" +dependencies = [ + "cfg-if 0.1.10", + "rand 0.7.3", + "static_assertions 1.1.0", +] + [[package]] name = "typenum" version = "1.12.0" @@ -1922,6 +2375,17 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "url" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" +dependencies = [ + "idna 0.1.5", + "matches", + "percent-encoding 1.0.1", +] + [[package]] name = "url" version = "2.2.1" @@ -1929,9 +2393,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ccd964113622c8e9322cfac19eb1004a07e636c545f325da085d5cdde6f1f8b" dependencies = [ "form_urlencoded", - "idna", + "idna 0.2.2", "matches", - "percent-encoding", + "percent-encoding 2.1.0", +] + +[[package]] +name = "uuid" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90dbc611eb48397705a6b0f6e917da23ae517e4d127123d2cf7674206627d32a" + +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom 0.2.2", + "serde", ] [[package]] @@ -1946,6 +2426,12 @@ version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + [[package]] name = "want" version = "0.3.0" @@ -1974,7 +2460,7 @@ version = "0.2.71" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ee1280240b7c461d6a0071313e08f34a60b0365f14260362e5a2b17d1d31aa7" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "serde", "serde_json", "wasm-bindgen-macro", @@ -2001,7 +2487,7 @@ version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e67a5806118af01f0d9045915676b22aaebecf4178ae7021bc171dab0b897ab" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "js-sys", "wasm-bindgen", "web-sys", diff --git a/Cargo.toml b/Cargo.toml index c188f23..06eff3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,12 @@ bollard = "0.10.1" rand = "0.8.0" tokio = { version = "1.2.0", features = ["full"] } anyhow = "1.0.37" +lazy-init = "0.3.0" +lazy_static = "1.4.0" +regex = "*" +r2d2 = "*" +r2d2_mysql = "16.0.0" +uuid = { version = "0.8", features = ["serde", "v4"] } async-trait = "0.1.42" futures = "0.3.12" serde = { version = "1.0", features = ["derive"] } @@ -25,3 +31,10 @@ hyper = { version = "0.14", features = [ "full" ] } serde_json = "1.0" reqwest = { version = "0.11", features = ["json"] } once_cell = "1.7.2" +strum = { version = "0.20", features = ["derive"] } +strum_macros = "*" + + +[dependencies.diesel] +features = ["mysql", "r2d2","chrono"] +version = "1.4.5" \ No newline at end of file diff --git a/diesel.toml b/diesel.toml new file mode 100644 index 0000000..92267c8 --- /dev/null +++ b/diesel.toml @@ -0,0 +1,5 @@ +# For documentation on how to configure this file, +# see diesel.rs/guides/configuring-diesel-cli + +[print_schema] +file = "src/schema.rs" diff --git a/migrations/.gitkeep b/migrations/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/config.rs b/src/config.rs index cbd5c13..927d3ee 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,8 +1,10 @@ use once_cell::sync::Lazy; pub static ENV_CONFIG: Lazy = Lazy::new(load_config); +#[derive(Clone, Default)] pub struct Config { pub database_url: String, + pub database_pool_size: u32, pub judge_container_port: String, pub docker_address: String, } @@ -11,12 +13,17 @@ pub fn load_config() -> Config { const DEFAULT_JUDGE_CONTAINER_PORT: &str = "8000"; const DEFAULT_DOCKER_ADDRESS: &str = "http://localhost:2375"; let database_url = dotenv::var("DATABASE_URL").unwrap(); + let database_pool_size = dotenv::var("DATABASE_POOL_SIZE") + .unwrap() + .parse::() + .unwrap_or(6); let judge_container_port = dotenv::var("JUDGE_CONTAINER_PORT") .unwrap_or_else(|_| DEFAULT_JUDGE_CONTAINER_PORT.to_string()); let docker_address = dotenv::var("DOCKER_ADDRESS").unwrap_or_else(|_| DEFAULT_DOCKER_ADDRESS.to_string()); Config { database_url, + database_pool_size, judge_container_port, docker_address, } diff --git a/src/domain.rs b/src/domain.rs new file mode 100644 index 0000000..908db95 --- /dev/null +++ b/src/domain.rs @@ -0,0 +1,2 @@ +pub mod model; +pub mod service; diff --git a/src/domain/model.rs b/src/domain/model.rs new file mode 100644 index 0000000..45c16cd --- /dev/null +++ b/src/domain/model.rs @@ -0,0 +1,4 @@ +mod model; +pub use identify::*; +mod identify; +pub use model::*; diff --git a/src/domain/model/identify.rs b/src/domain/model/identify.rs new file mode 100644 index 0000000..1c68221 --- /dev/null +++ b/src/domain/model/identify.rs @@ -0,0 +1,16 @@ +use serde::*; +use Default; +#[derive(Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Debug, Queryable)] +pub struct ProblemId(pub i64); +#[derive(Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Debug, Queryable)] +pub struct SubmitId(pub i64); +#[derive(Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Debug)] +pub struct TestcaseId(pub i64); +#[derive(Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Debug)] +pub struct TestcaseResultId(pub i64); +#[derive(Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Debug)] +pub struct TestcaseSetsId(pub i64); +#[derive(Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Debug)] +pub struct ContestId(pub i64); +#[derive(Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Debug, Queryable)] +pub struct UserId(pub i64); diff --git a/src/domain/model/model.rs b/src/domain/model/model.rs new file mode 100644 index 0000000..4d8a3fc --- /dev/null +++ b/src/domain/model/model.rs @@ -0,0 +1,172 @@ +use crate::domain::model::*; +use chrono::NaiveDateTime; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, fmt, fmt::Debug}; +use strum_macros::EnumString; +use uuid::Uuid; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Submit { + pub id: SubmitId, + pub user_id: UserId, + pub problem_id: ProblemId, + pub path: String, + pub status: Status, + pub point: Option, + pub execution_time: Option, + pub execution_memory: Option, + pub compile_error: Option, + pub lang: String, + pub created_at: NaiveDateTime, + pub updated_at: NaiveDateTime, + pub deleted_at: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Problem { + pub id: ProblemId, + pub slug: Option, + pub name: Option, + pub contest_id: Option, + pub writer_user_id: UserId, + pub position: Option, + pub uuid: Option, + pub difficulty: String, + pub execution_time_limit: i32, + pub statement: String, + pub constraints: String, + pub input_format: String, + pub output_format: String, + pub checker_path: Option, + pub created_at: NaiveDateTime, + pub updated_at: NaiveDateTime, + pub deleted_at: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Testcase { + pub id: TestcaseId, + pub problem_id: ProblemId, + pub name: Option, + pub input: Option, + pub output: Option, + pub explanation: Option, + pub created_at: NaiveDateTime, + pub updated_at: NaiveDateTime, + pub deleted_at: Option, +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct TestcaseResult { + pub status: Status, + pub cmd_result: CmdResult, +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct CmdResult { + pub execution_time: i32, // ms + pub stdout_size: usize, // byte + pub execution_memory: i32, // KB + pub ok: bool, // exit_code == 0 + pub message: String, // コンパイルメッセージ +} + +#[allow(dead_code)] +pub struct TestcaseSets { + pub id: TestcaseSetsId, + pub points: i64, +} + +#[allow(dead_code)] +pub struct TestcaseTestcaseSets { + pub testcase_id: TestcaseId, + pub testcase_set_id: i64, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct CompileRequest { + pub submit_id: SubmitId, + pub cmd: String, // コンパイルコマンド or 実行コマンド +} + +#[derive(Serialize, Deserialize)] +pub struct CompileResponse(pub CmdResult); + +#[derive(Serialize, Deserialize, Debug)] +pub struct DownloadRequest { + pub submit_id: SubmitId, + pub code_path: String, // gcp 上のパス + pub filename: String, // Main.ext +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct JudgeRequest { + pub submit_id: SubmitId, + pub cmd: String, // コンパイルコマンド or 実行コマンド + pub time_limit: i32, // 実行制限時間 + pub mem_limit: i32, // メモリ制限 + pub testcases: Vec, // pub testcase: Testcase, + pub problem: Problem, // pub problem: Problem, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct JudgeResponse { + pub submit_id: SubmitId, + pub status: Status, + pub score: i64, + pub execution_time: i32, + pub execution_memory: i32, + pub testcase_result_map: HashMap, +} + +#[allow(clippy::unknown_clippy_lints)] +#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, EnumString)] +pub enum Status { + AC, + TLE, + MLE, + OLE, + WA, + RE, + CE, + IE, + WR, + WIP, +} + +impl fmt::Display for Status { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let s = match *self { + Status::AC => "AC", + Status::TLE => "TLE", + Status::MLE => "MLE", + Status::OLE => "OLE", + Status::WA => "WA", + Status::RE => "RE", + Status::CE => "CE", + Status::IE => "IE", + Status::WR => "WR", + Status::WIP => "WIP", + }; + + write!(f, "{}", s) + } +} + +impl Status { + #[allow(dead_code)] + pub fn to_priority(&self) -> i32 { + match *self { + Status::AC => 1, + Status::TLE => 2, + Status::MLE => 3, + Status::OLE => 4, + Status::WA => 5, + Status::RE => 6, + Status::CE => 7, + Status::IE => 8, + Status::WR => 9, + Status::WIP => 10, + } + } +} diff --git a/src/domain/service.rs b/src/domain/service.rs new file mode 100644 index 0000000..e46b2cc --- /dev/null +++ b/src/domain/service.rs @@ -0,0 +1,4 @@ +pub mod judge_runner_service; +pub use judge_runner_service::*; +pub mod judge_task_service; +pub use judge_task_service::*; diff --git a/src/domain/service/judge_runner_service.rs b/src/domain/service/judge_runner_service.rs new file mode 100644 index 0000000..5709f9b --- /dev/null +++ b/src/domain/service/judge_runner_service.rs @@ -0,0 +1,214 @@ +use crate::domain::model::*; +use crate::domain::service::*; +use crate::initializer::*; +use crate::lang_cmd::LANG_CMD; +use crate::repositories::*; +use crate::utils::{self}; +use crate::wrapper::*; +use bollard::Docker; +use chrono::*; +use futures::stream; +use futures::*; +use reqwest::{Client, StatusCode}; +use std::sync::Arc; +use std::time::Duration; +use tokio::time::sleep; + +#[derive(Clone)] +pub struct JudgeTaskRunner { + problem_repo: ProblemsRepository, + submit_repo: SubmitRepository, + testcase_repo: TestcasesRepository, + testcase_result_repo: TestcasesResultRepository, + docker_conn: Arc, + http_client: Client, +} + +const INTERVAL: Duration = Duration::from_secs(1); +const MEM_LIMIT: i32 = 1_024_000; // 1,024,000KB(1,024KB) +impl JudgeTaskRunner { + pub fn new(app: App, docker_conn: Arc, http_client: Client) -> Self { + Self { + problem_repo: app.repositories.problem_repo.as_ref().clone(), + submit_repo: app.repositories.submit_repo.as_ref().clone(), + testcase_repo: app.repositories.testcase_repo.as_ref().clone(), + testcase_result_repo: app.repositories.testcase_result_repo.as_ref().clone(), + docker_conn, + http_client, + } + } + + pub async fn gen_job(&self) -> Result<(), GeneralError> { + // この `task` が 1 実行単位 + let task = || { + async move { + let task: JudgeTask = JudgeTask::new( + self.problem_repo.clone(), + self.submit_repo.clone(), + self.testcase_repo.clone(), + self.testcase_result_repo.clone(), + self.docker_conn.clone(), + self.http_client.clone(), + ); + // 提出を取得 + let submit = match task.fetch_submit().await { + Ok(s) => s, + Err(_) => { + // TODO(magurotuna): 提出が取得できなかった場合は 1 秒待って次の実行に移る + sleep(INTERVAL).await; + return GeneralError::only("Couldn't find an unjudged submit."); + } + }; + // TODO(magurotuna): コンテナ名をちゃんとする UUIDを発行? + let container_name = format!("DUMMY_NAME_{}", utils::gen_rand_string(6)); + + match self.execute_task(&task, &submit, &container_name).await { + Ok(()) => (), + Err(e) => { + eprint!("{}", e.error_type()); + sleep(INTERVAL).await; + match task.save_internal_error(submit.id).await { + Ok(_) => (()), + _ => { + return GeneralError::only("internal error"); + } + }; + //task.remove_container(&container_name).await?; + return GeneralError::only("internal error"); + } + } + GeneralError::only("ok") + } + }; + + // stream::unfold をすることで、1 実行単位である `task` を延々と繰り返すような Stream を作る + let mut stream = stream::unfold((), move |_| { + // カッコが続いて見づらくなるので Unit に置き換えて多少見やすくなるようにしている + + const UNIT: () = (); + task().map(|task_result| Some((task_result, UNIT))) + }) + .boxed(); + + while let Some(_task_result) = stream.next().await { + // 1回1回の task の実行結果を使って何かやりたければここに書く + // ログ出力とか? + } + Ok(()) + } + async fn execute_task( + &self, + task: &JudgeTask, + submit: &Submit, + container_name: &str, + ) -> Result<(), GeneralError> { + let command = match LANG_CMD.get(&submit.lang) { + Some(command) => command, + None => { + // statusをIEへ + task.save_internal_error(submit.id.clone()).await?; + + return Err(GeneralError::only( + "Couldn't find a language command setting.", + )); + } + }; + + // リジャッジなら過去の testcase_results をすべて消す。 + match submit.status { + Status::WR => { + task.delete_testcase_results(&submit.id).await?; + } + _ => (), + } + + // コンテナ作成 + + let (_container, ip_addr) = match task.create_container(&container_name).await { + Ok((c, i)) => (c, i), + Err(e) => return Err(GeneralError::new(DockerError::InternalError, e)), + }; + // テストケース、問題を取得 + + let problem = task.fetch_problem(&submit.problem_id).await?; + let testcases = task.fetch_testcases(&submit.problem_id).await?; + + let req = self.generate_judge_request(submit.id.clone(), &command.run, problem, testcases); + + let download_response = task + .request_download( + &ip_addr, + &DownloadRequest { + submit_id: submit.id.clone(), + code_path: submit.path.clone(), + filename: command.file_name.clone(), + }, + ) + .await?; + + if download_response.status() != StatusCode::OK { + return Err(GeneralError::only("Download failed")); + } + + let compile_response = task + .request_compile( + &ip_addr, + &CompileRequest { + submit_id: submit.id.clone(), + cmd: command.compile.clone(), + }, + ) + .await?; + // コンパイルエラーはコンテナの中で処理をしているはずなので ok + if !compile_response.0.ok { + return Ok(()); + } + + let _judge_response = task.request_judge(&ip_addr, &req).await?; + // TODO judgeレスポンスによる処理 + // コンテナを削除 + task.remove_container(&container_name).await?; + + Ok(()) + } + fn generate_judge_request( + &self, + submit_id: SubmitId, + cmd: &str, + problem: Problem, + testcases: Vec, + ) -> JudgeRequest { + let request_testcases = testcases + .iter() + .map(|t| Testcase { + id: t.id.clone(), + input: None, + output: None, + explanation: None, + problem_id: ProblemId(-1), + name: Some(t.name.clone().unwrap_or_default()), + created_at: Local::now().naive_local(), + updated_at: Local::now().naive_local(), + deleted_at: None, + }) + .collect(); + let request_problem = Problem { + id: problem.id, + uuid: Some(problem.uuid.unwrap_or_default()), + checker_path: Some( + problem + .checker_path + .unwrap_or_else(|| "checker_path/wcmp.cpp".to_string()), + ), + ..problem + }; + JudgeRequest { + submit_id, + cmd: cmd.to_string(), + time_limit: problem.execution_time_limit, + mem_limit: MEM_LIMIT, + testcases: request_testcases, + problem: request_problem, + } + } +} diff --git a/src/domain/service/judge_task_service.rs b/src/domain/service/judge_task_service.rs new file mode 100644 index 0000000..cfd7df5 --- /dev/null +++ b/src/domain/service/judge_task_service.rs @@ -0,0 +1,222 @@ +use crate::config::ENV_CONFIG; +use crate::domain::model::*; +use crate::repositories::*; +use crate::wrapper::*; +use anyhow::Result; +use bollard::{ + container::{Config, CreateContainerOptions, RemoveContainerOptions, StartContainerOptions}, + models::HostConfig, + service::ContainerCreateResponse, + Docker, +}; +use reqwest::{header, Client, Response, StatusCode}; +use std::sync::Arc; +use std::time::Duration; +pub struct JudgeTask { + pub problem_repo: ProblemsRepository, + pub submit_repo: SubmitRepository, + pub testcase_repo: TestcasesRepository, + pub testcase_result_repo: TestcasesResultRepository, + pub docker_conn: Arc, + pub http_client: Client, +} + +/// 1つの submit に対するジャッジの処理を担当する +impl JudgeTask { + pub fn new( + problem_repo: ProblemsRepository, + submit_repo: SubmitRepository, + testcase_repo: TestcasesRepository, + testcase_result_repo: TestcasesResultRepository, + docker_conn: Arc, + http_client: Client, + ) -> Self { + Self { + problem_repo, + submit_repo, + testcase_repo, + testcase_result_repo, + docker_conn, + http_client, + } + } + + /// 未ジャッジの提出のうち、もっとも古いもの1件を取得する。 + /// その1件のステータスを「ジャッジ中」にする + pub async fn fetch_submit(&self) -> Result { + let mut submit = self.submit_repo.pop_queing_submit().await?; + submit.status = Status::WIP; + self.submit_repo.save(submit.clone()).await?; + Ok(submit.clone()) + } + pub async fn fetch_problem(&self, problem_id: &ProblemId) -> Result { + Ok(self.problem_repo.find_by_id(problem_id).await?) + } + + pub async fn fetch_testcases( + &self, + problem_id: &ProblemId, + ) -> Result, GeneralError> { + Ok(self.testcase_repo.find_by_problem_id(problem_id).await?) + } + + pub async fn delete_testcase_results(&self, submit_id: &SubmitId) -> Result<(), GeneralError> { + Ok(self.testcase_result_repo.logical_delete(submit_id).await?) + } + + /// Docker コンテナを指定された名前で立ち上げる + pub async fn create_container(&self, name: &str) -> Result<(ContainerCreateResponse, String)> { + const IMAGE: &str = "cafecoder_docker:2104"; + + let options = Some(CreateContainerOptions { name }); + let config = Config { + image: Some(IMAGE), + host_config: Some(HostConfig { + memory: Some(2_147_483_648_i64), + pids_limit: Some(512_i64), + privileged: Some(true), + ..Default::default() + }), + ..Default::default() + }; + let res = self.docker_conn.create_container(options, config).await?; + + self.docker_conn + .start_container(name, None::>) + .await?; + let inspect = self.docker_conn.inspect_container(name, None).await?; + + let network_settings = inspect + .network_settings + .expect("couldn't get network_settings"); + let ip_addr = network_settings + .ip_address + .expect("couldn't get IP address"); + + // TODO: コンテナが立ち上がったかどうかのチェック + tokio::time::sleep(Duration::new(1, 0)).await; + + Ok((res, ip_addr)) + } + + pub async fn request_compile( + &self, + ip_addr: &str, + req: &CompileRequest, + ) -> Result { + dbg!(serde_json::to_string(req).unwrap()); + let resp = match self + .http_client + .post(&format!( + "http://{}:{}/compile", + &ip_addr, &ENV_CONFIG.judge_container_port + )) + .json(&req) + .header(header::CONTENT_TYPE, "application/json") + .send() + .await + { + Ok(r) => r, + _ => { + return Err(GeneralError::only("request_compile_error")); + } + }; + + if resp.status() != StatusCode::OK { + GeneralError::only("compile: response status code was not 200 OK"); + } + + match resp.json().await { + Ok(r) => Ok(r), + _ => { + return Err(GeneralError::only("request_compile_serialize_error")); + } + } + } + + pub async fn request_download( + &self, + ip_addr: &str, + req: &DownloadRequest, + ) -> Result { + dbg!(serde_json::to_string(req).unwrap()); + let resp = match self + .http_client + .post(&format!( + "http://{}:{}/download", + &ip_addr, &ENV_CONFIG.judge_container_port + )) + .json(&req) + .header(header::CONTENT_TYPE, "application/json") + .send() + .await + { + Ok(r) => r, + _ => { + return Err(GeneralError::only("request_download_error")); + } + }; + + Ok(resp) + } + pub async fn request_judge( + &self, + ip_addr: &str, + req: &JudgeRequest, + ) -> Result { + dbg!(serde_json::to_string(req).unwrap()); + let resp = match self + .http_client + .post(&format!( + "http://{}:{}/judge", + &ip_addr, &ENV_CONFIG.judge_container_port + )) + .header(header::CONTENT_TYPE, "application/json") + .json(&req) + .send() + .await + { + Ok(r) => r, + _ => { + return Err(GeneralError::only("request_judge_error")); + } + }; + + if resp.status() != StatusCode::OK { + GeneralError::only("judge: response status code was not 200 OK\n"); + } + + match resp.json().await { + Ok(r) => Ok(r), + _ => { + return Err(GeneralError::only("request_judge_serialize_error")); + } + } + } + + /// Docker コンテナを削除する + pub async fn remove_container(&self, name: &str) -> Result<(), GeneralError> { + let options = RemoveContainerOptions { + force: true, + ..Default::default() + }; + match self.docker_conn.remove_container(name, Some(options)).await { + Ok(r) => r, + _ => { + return Err(GeneralError::only("remove_container_error")); + } + }; + Ok(()) + } + + pub async fn save_internal_error(&self, submit_id: SubmitId) -> Result<(), GeneralError> { + let mut submit = self.submit_repo.find_by_id(&submit_id).await?; + submit.status = Status::IE; + match self.submit_repo.save(submit).await { + Ok(_) => Ok(()), + _ => { + return Err(GeneralError::only("save_internal_error")); + } + } + } +} diff --git a/src/entities.rs b/src/entities.rs deleted file mode 100644 index 7c0e179..0000000 --- a/src/entities.rs +++ /dev/null @@ -1,52 +0,0 @@ -use chrono::NaiveDateTime; -use sqlx::FromRow; -#[derive(Debug, FromRow)] -pub struct Submit { - pub id: i64, - pub user_id: i64, - pub problem_id: i64, - pub path: String, - pub status: String, - pub point: Option, - pub execution_time: Option, - pub execution_memory: Option, - pub compile_error: Option, - pub lang: String, - pub created_at: NaiveDateTime, - pub updated_at: NaiveDateTime, - pub deleted_at: Option, -} - -#[derive(Debug, FromRow)] -pub struct Problem { - pub id: i64, - pub slug: Option, - pub name: Option, - pub contest_id: Option, - pub writer_user_id: i64, - pub checker_path: Option, - pub execution_time_limit: i32, - pub position: Option, - pub uuid: Option, - pub difficulty: String, - pub statement: String, - pub constraints: String, - pub input_format: String, - pub output_format: String, - pub created_at: NaiveDateTime, - pub updated_at: NaiveDateTime, - pub deleted_at: Option, -} - -#[derive(Debug, FromRow)] -pub struct Testcase { - pub id: i64, - pub problem_id: i64, - pub name: Option, - pub input: Option, - pub output: Option, - pub explanation: Option, - pub created_at: NaiveDateTime, - pub updated_at: NaiveDateTime, - pub deleted_at: Option, -} diff --git a/src/db.rs b/src/infra.rs similarity index 80% rename from src/db.rs rename to src/infra.rs index 00dcdbe..7628c1b 100644 --- a/src/db.rs +++ b/src/infra.rs @@ -1,11 +1,9 @@ -use anyhow::Result; -use sqlx::{mysql::MySqlPool, MySql}; -pub type DbPool = sqlx::Pool; +mod connection_pool; +mod db_executor; +pub use db_executor::*; -pub async fn new_pool(database_url: &str) -> Result { - let pool = MySqlPool::connect(database_url).await?; - Ok(pool) -} +#[derive(Debug)] +pub enum DatabaseError {} #[cfg(feature = "db_test")] #[cfg(test)] diff --git a/src/infra/connection_pool.rs b/src/infra/connection_pool.rs new file mode 100644 index 0000000..f4c4245 --- /dev/null +++ b/src/infra/connection_pool.rs @@ -0,0 +1,42 @@ +use diesel::{mysql::MysqlConnection, r2d2}; +use lazy_init::LazyTransform; +use std::sync::Arc; + +#[derive(Clone)] +pub struct MySQLConnPool( + Arc>>>, +); + +struct Config { + database_url: String, + size_conn_pool: u32, +} + +fn initialize(config: Config) -> r2d2::Pool> { + let manager = r2d2::ConnectionManager::::new(config.database_url); + r2d2::Pool::builder() + .max_size(config.size_conn_pool) + .build(manager) + .expect("Failed to create pool") +} + +impl MySQLConnPool { + pub fn new(database_url: String, size_conn_pool: u32) -> MySQLConnPool { + MySQLConnPool(Arc::new(LazyTransform::new(Config { + database_url, + size_conn_pool, + }))) + } + + #[allow(dead_code)] + pub fn ensure_initialized(&self) { + self.get_connection(); + } + + #[allow(dead_code)] + pub fn get_connection( + &self, + ) -> r2d2::PooledConnection> { + self.0.get_or_create(initialize).get().unwrap() + } +} diff --git a/src/infra/db_executor.rs b/src/infra/db_executor.rs new file mode 100644 index 0000000..58aa513 --- /dev/null +++ b/src/infra/db_executor.rs @@ -0,0 +1,135 @@ +use crate::error::*; +use crate::infra::connection_pool::MySQLConnPool; + +#[derive(Debug)] +pub enum DBExecutorError { + DBError, + RecordNotFound, +} + +impl IGeneralError for DBExecutorError { + fn error_type(&self) -> String { + use DBExecutorError::*; + + match self { + DBError => "db_error", + RecordNotFound => "record_not_found", + } + .to_string() + } +} + +impl From for GeneralError { + fn from(err: diesel::result::Error) -> GeneralError { + use diesel::result::Error::*; + + match err { + NotFound => GeneralError::new(DBExecutorError::RecordNotFound, err), + _ => GeneralError::new(DBExecutorError::DBError, err), + } + } +} + +#[derive(Clone)] +pub struct DBExecutor(MySQLConnPool); + +impl DBExecutor { + pub fn new(database_url: String, size_conn_pool: u32) -> DBExecutor { + DBExecutor(MySQLConnPool::new(database_url, size_conn_pool)) + } + + pub fn get_connection( + &self, + ) -> r2d2::PooledConnection> { + self.0.get_connection() + } +} + +#[derive(Clone)] +pub struct DBConnector(DBExecutor); + +impl DBConnector { + pub fn new(executor: DBExecutor) -> DBConnector { + DBConnector(executor) + } + + #[allow(dead_code)] + pub async fn ensure_initialized(&self) -> Result<(), GeneralError> { + let executor = self.0.clone(); + + tokio::task::spawn_blocking(move || { + executor.0.ensure_initialized(); + Ok(()) + }) + .await? + } + + pub async fn execute(&self, query: Q) -> Result + where + Q: diesel::RunQueryDsl, + Q: diesel::query_builder::QueryFragment, + Q: diesel::query_builder::QueryId, + { + let conn = self.0.get_connection(); + + tokio::task::spawn_blocking(move || { + let result = query.execute(&conn)?; + Ok(result) + }) + .await? + } + + pub async fn first( + &self, + query: Q, + ) -> Result + where + Q: diesel::query_dsl::limit_dsl::LimitDsl, + Q: diesel::RunQueryDsl, + diesel::helper_types::Limit: diesel::query_dsl::LoadQuery, + { + let conn = self.0.get_connection(); + + tokio::task::spawn_blocking(move || { + let result = query.first(&conn)?; + Ok(result) + }) + .await? + } + + pub async fn load( + &self, + query: Q, + ) -> Result, GeneralError> + where + Q: diesel::RunQueryDsl, + Q: diesel::query_dsl::LoadQuery, + { + let conn = self.0.get_connection(); + + tokio::task::spawn_blocking(move || { + let result = query.load(&conn)?; + Ok(result) + }) + .await? + } + + #[allow(dead_code)] + pub async fn load_sql< + T: 'static + Send + diesel::deserialize::QueryableByName, + >( + &self, + query: impl Into, + ) -> Result, GeneralError> { + let conn = self.0.get_connection(); + let q = query.into(); + + tokio::task::spawn_blocking(move || { + use diesel::prelude::*; + + let result = diesel::sql_query(q).load::(&conn)?; + Ok(result) + }) + .await? + } +} diff --git a/src/initializer.rs b/src/initializer.rs new file mode 100644 index 0000000..15b50df --- /dev/null +++ b/src/initializer.rs @@ -0,0 +1,39 @@ +use crate::config::Config; +use crate::infra::*; +use crate::repositories::{self, *}; +use std::sync::Arc; + +#[derive(Clone)] +pub struct Repositories { + pub problem_repo: Arc, + pub submit_repo: Arc, + pub testcase_repo: Arc, + pub testcase_result_repo: Arc, +} + +#[derive(Clone)] +pub struct App { + pub config: Config, + pub repositories: Repositories, +} + +impl App { + pub fn new(conf: Config) -> Self { + let pool = DBExecutor::new(conf.clone().database_url, conf.clone().database_pool_size); + App { + config: conf.clone(), + repositories: Repositories::new(DBConnector::new(pool)), + } + } +} + +impl Repositories { + pub fn new(dbconn: DBConnector) -> Self { + Self { + problem_repo: Arc::new(ProblemsRepository::new(dbconn.clone())), + submit_repo: Arc::new(SubmitRepository::new(dbconn.clone())), + testcase_repo: Arc::new(TestcasesRepository::new(dbconn.clone())), + testcase_result_repo: Arc::new(TestcasesResultRepository::new(dbconn.clone())), + } + } +} diff --git a/src/main.rs b/src/main.rs index c386218..2a6aea9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,50 +1,56 @@ mod config; -mod db; -mod entities; +mod initializer; mod lang_cmd; -mod models; -mod repository; -mod task; +mod schema; mod utils; +use crate::config::*; +use crate::initializer::App; use anyhow::Result; use bollard::API_DEFAULT_VERSION; -use config::ENV_CONFIG; use core::time::Duration; use futures::future::join_all; +//use futures::future::join_all; use std::sync::Arc; +#[macro_use] +mod wrapper; +pub use wrapper::error::*; +pub use wrapper::*; +#[macro_use] +extern crate diesel; + +#[macro_use] +mod domain; +mod infra; +mod repositories; // TODO(magurotuna): ここの値も要検討 const JOB_THREADS: usize = 3; const HTTP_TIMEOUT: u64 = 180; - // TODO(magurotuna): スレッド数指定を柔軟に行うため、Tokio の RuntimeBuilder を使うよう書き換える #[tokio::main(worker_threads = 4)] async fn main() -> Result<()> { - let db_conn = Arc::new(db::new_pool(&ENV_CONFIG.database_url).await?); - /* - let docker_conn = Arc::new(bollard::Docker::connect_with_http( - &ENV_CONFIG.docker_address, - 4, - API_DEFAULT_VERSION, - )?); - */ - let docker_conn = Arc::new(bollard::Docker::connect_with_unix( - "/var/run/docker.sock", - 10, - API_DEFAULT_VERSION, - )?); - let http_client = reqwest::Client::builder() - .timeout(Duration::new(HTTP_TIMEOUT, 0)) - .build()?; - let mut handles = Vec::new(); + for _ in 0..JOB_THREADS { - let handle = tokio::spawn(task::gen_job( - db_conn.clone(), - docker_conn.clone(), - http_client.clone(), - )); - handles.push(handle); + handles.push(tokio::spawn(async move { + let conf = load_config(); + let http_client = reqwest::Client::builder() + .timeout(Duration::new(HTTP_TIMEOUT, 0)) + .build() + .unwrap(); + let app = App::new(conf); + let docker_conn = Arc::new( + bollard::Docker::connect_with_unix("/var/run/docker.sock", 10, API_DEFAULT_VERSION) + .unwrap(), + ); + let task = domain::service::JudgeTaskRunner::new( + app.clone(), + docker_conn.clone(), + http_client, + ); + + task.gen_job().await + })); } join_all(handles).await; diff --git a/src/models.rs b/src/models.rs deleted file mode 100644 index b56da69..0000000 --- a/src/models.rs +++ /dev/null @@ -1,2 +0,0 @@ -#[path = "./../cafecoder-docker-rs/src/models.rs"] -pub mod container; diff --git a/src/repositories.rs b/src/repositories.rs new file mode 100644 index 0000000..a74b5b4 --- /dev/null +++ b/src/repositories.rs @@ -0,0 +1,11 @@ +mod problem_repo; +pub use problem_repo::*; + +mod submit_repo; +pub use submit_repo::*; + +mod testcase_repo; +pub use testcase_repo::*; + +mod testcase_result_repo; +pub use testcase_result_repo::*; diff --git a/src/repositories/problem_repo.rs b/src/repositories/problem_repo.rs new file mode 100644 index 0000000..92c57fb --- /dev/null +++ b/src/repositories/problem_repo.rs @@ -0,0 +1,107 @@ +use crate::domain::model::*; +use crate::infra::*; +use crate::schema::*; +use crate::wrapper::*; +use async_trait::async_trait; +use chrono::NaiveDateTime; +use diesel::prelude::*; +use uuid::*; + +#[derive(Clone)] +pub struct ProblemsRepository { + db: DBConnector, +} + +#[derive(Clone, Queryable)] +pub struct ProblemRecord { + id: i64, + slug: Option, + name: Option, + contest_id: Option, + writer_user_id: i64, + position: Option, + uuid: Option, + difficulty: String, + execution_time_limit: i32, + statement: String, + constraints: String, + input_format: String, + output_format: String, + checker_path: Option, + created_at: NaiveDateTime, + updated_at: NaiveDateTime, + deleted_at: Option, +} + +impl ProblemRecord { + #[allow(dead_code)] + fn from_model(problem: Problem) -> ProblemRecord { + ProblemRecord { + id: problem.id.0, + slug: problem.slug, + name: problem.name, + contest_id: problem.contest_id.map(|c| c.0), + writer_user_id: problem.writer_user_id.0, + position: problem.position, + uuid: problem.uuid.map(|u| u.to_hyphenated().to_string()), + difficulty: problem.difficulty, + execution_time_limit: problem.execution_time_limit, + statement: problem.statement, + constraints: problem.constraints, + input_format: problem.input_format, + output_format: problem.output_format, + checker_path: problem.checker_path, + created_at: problem.created_at, + updated_at: problem.updated_at, + deleted_at: problem.deleted_at, + } + } + fn to_model(&self) -> Problem { + Problem { + id: ProblemId(self.id), + slug: self.slug.clone(), + name: self.name.clone(), + contest_id: self.contest_id.map(|c| ContestId(c)), + writer_user_id: UserId(self.writer_user_id), + position: self.position.clone(), + uuid: self + .uuid + .clone() + .map(|u| Uuid::parse_str(&u).unwrap_or(Uuid::new_v4())), + difficulty: self.difficulty.clone(), + execution_time_limit: self.execution_time_limit, + statement: self.statement.clone(), + constraints: self.constraints.clone(), + input_format: self.input_format.clone(), + output_format: self.output_format.clone(), + checker_path: self.checker_path.clone(), + created_at: self.created_at, + updated_at: self.updated_at, + deleted_at: self.deleted_at, + } + } +} + +#[async_trait] +pub trait IProblemRepository { + fn new(db: DBConnector) -> Self; + async fn find_by_id(&self, problem_id: &ProblemId) -> Result; +} + +#[async_trait] +impl IProblemRepository for ProblemsRepository { + fn new(db: DBConnector) -> Self { + Self { db } + } + async fn find_by_id(&self, problem_id: &ProblemId) -> Result { + let record = self + .db + .first::( + problems::table + .filter(problems::id.eq(problem_id.0)) + .filter(problems::deleted_at.is_not_null()), + ) + .await?; + Ok(record.to_model()) + } +} diff --git a/src/repositories/submit_repo.rs b/src/repositories/submit_repo.rs new file mode 100644 index 0000000..5d46fea --- /dev/null +++ b/src/repositories/submit_repo.rs @@ -0,0 +1,165 @@ +use crate::domain::model::*; +use crate::infra::*; +use crate::schema::*; +use crate::wrapper::*; +use async_trait::async_trait; +use chrono::NaiveDateTime; +use diesel::prelude::*; +use diesel::replace_into; +use std::str::FromStr; + +#[derive(Clone)] +pub struct SubmitRepository { + db: DBConnector, +} + +#[derive(Clone, Queryable, Insertable)] +#[table_name = "submits"] +pub struct SubmitRecord { + pub id: i64, + pub user_id: i32, + pub problem_id: i64, + pub path: String, + pub status: String, + pub point: Option, + pub execution_time: Option, + pub execution_memory: Option, + pub compile_error: Option, + pub lang: String, + pub created_at: NaiveDateTime, + pub updated_at: NaiveDateTime, + pub deleted_at: Option, +} + +impl SubmitRecord { + pub fn from_model(submit: Submit) -> SubmitRecord { + SubmitRecord { + id: submit.id.0, + user_id: submit.user_id.0 as i32, + problem_id: submit.problem_id.0, + path: submit.path, + status: submit.status.to_string(), + point: submit.point, + execution_time: submit.execution_time, + execution_memory: submit.execution_memory, + compile_error: submit.compile_error, + lang: submit.lang, + created_at: submit.created_at, + updated_at: submit.updated_at, + deleted_at: submit.deleted_at, + } + } + pub fn to_model(self) -> Submit { + Submit { + id: SubmitId(self.id), + user_id: UserId(self.user_id as i64), + problem_id: ProblemId(self.problem_id), + path: self.path, + status: Status::from_str(&self.status).unwrap_or(Status::IE), + point: self.point, + execution_time: self.execution_time, + execution_memory: self.execution_memory, + compile_error: self.compile_error, + lang: self.lang, + created_at: self.created_at, + updated_at: self.updated_at, + deleted_at: self.deleted_at, + } + } +} + +#[async_trait] +pub trait ISubmitRepository { + fn new(db: DBConnector) -> Self; + async fn find_by_id(&self, submit_id: &SubmitId) -> Result; + async fn pop_queing_submit(&self) -> Result; + async fn save(&self, submit: Submit) -> Result<(), GeneralError>; +} + +#[async_trait] +impl ISubmitRepository for SubmitRepository { + fn new(db: DBConnector) -> Self { + Self { db } + } + async fn find_by_id(&self, submit_id: &SubmitId) -> Result { + Ok(self + .db + .first::(submits::table.filter(submits::id.eq(submit_id.0))) + .await? + .to_model()) + } + async fn pop_queing_submit(&self) -> Result { + let record = self + .db + .first::( + submits::table + .filter(submits::status.eq("WJ")) + .filter(submits::status.eq("WR")) + .filter(submits::deleted_at.is_not_null()) + .order_by(submits::updated_at.asc()), + ) + .await?; + Ok(record.to_model()) + } + async fn save(&self, submit: Submit) -> Result<(), GeneralError> { + self.db + .execute( + replace_into(submits::table) + .values::(SubmitRecord::from_model(submit)), + ) + .await?; + Ok(()) + } +} + +/* +let submits = sqlx::query_as( + r#" + SELECT + id + , user_id + , problem_id + , path + , status + , point + , execution_time + , execution_memory + , compile_error + , lang + , created_at + , updated_at + , deleted_at + FROM + submits + WHERE + AND deleted_at IS NULL + ORDER BY + updated_at ASC + LIMIT + 1 + FOR UPDATE + "#, +) +.fetch_one(self) +.await?; + +Ok(submits) +*/ + +/* +async fn update_status(&mut self, id: i64, status: &str) -> Result { + let result = sqlx::query!( + r#" + UPDATE submits + SET + status = ? + WHERE + id = ? + "#, + status, + id, + ) + .execute(self) + .await?; + Ok(result.rows_affected()) + */ diff --git a/src/repositories/testcase_repo.rs b/src/repositories/testcase_repo.rs new file mode 100644 index 0000000..45aee10 --- /dev/null +++ b/src/repositories/testcase_repo.rs @@ -0,0 +1,86 @@ +use crate::domain::model::*; +use crate::infra::*; +use crate::schema::*; +use crate::wrapper::*; +use async_trait::async_trait; +use chrono::NaiveDateTime; +use diesel::prelude::*; + +#[derive(Clone)] +pub struct TestcasesRepository { + db: DBConnector, +} + +#[derive(Clone, Queryable)] +pub struct TestcaseRecord { + id: i64, + problem_id: i64, + name: Option, + input: Option, + output: Option, + explanation: Option, + created_at: NaiveDateTime, + updated_at: NaiveDateTime, + deleted_at: Option, +} + +impl TestcaseRecord { + #[allow(dead_code)] + fn from_model(testcase: Testcase) -> TestcaseRecord { + TestcaseRecord { + id: testcase.id.0, + problem_id: testcase.problem_id.0, + name: testcase.name, + input: testcase.input, + output: testcase.output, + explanation: testcase.explanation, + created_at: testcase.created_at, + updated_at: testcase.updated_at, + deleted_at: testcase.deleted_at, + } + } + + fn to_model(&self) -> Testcase { + Testcase { + id: TestcaseId(self.id), + problem_id: ProblemId(self.problem_id), + name: self.name.clone(), + input: self.input.clone(), + output: self.output.clone(), + explanation: self.explanation.clone(), + created_at: self.created_at, + updated_at: self.updated_at, + deleted_at: self.deleted_at, + } + } +} + +#[async_trait] +pub trait ITestcaseRepository { + fn new(db: DBConnector) -> Self; + async fn find_by_problem_id( + &self, + problem_id: &ProblemId, + ) -> Result, GeneralError>; +} + +#[async_trait] +impl ITestcaseRepository for TestcasesRepository { + fn new(db: DBConnector) -> Self { + Self { db } + } + async fn find_by_problem_id( + &self, + problem_id: &ProblemId, + ) -> Result, GeneralError> { + let records = self + .db + .load::( + testcases::table + .filter(testcases::problem_id.eq(problem_id.0)) + .filter(testcases::deleted_at.is_not_null()), + ) + .await?; + Ok(records.into_iter().map(|r| r.to_model()).collect()) + } +} diff --git a/src/repositories/testcase_result_repo.rs b/src/repositories/testcase_result_repo.rs new file mode 100644 index 0000000..156e370 --- /dev/null +++ b/src/repositories/testcase_result_repo.rs @@ -0,0 +1,83 @@ +use crate::domain::model::*; +use crate::infra::*; +use crate::schema::*; +use crate::wrapper::*; +use async_trait::async_trait; +use chrono::Local; +use chrono::NaiveDateTime; +use diesel::prelude::*; +use diesel::update; + +#[derive(Clone)] +pub struct TestcasesResultRepository { + db: DBConnector, +} + +#[derive(Clone, Queryable)] +pub struct TestcaseResultRecord { + id: i64, + submit_id: i64, + testcase_id: i64, + status: String, + execution_time: i32, + execution_memory: i32, + created_at: NaiveDateTime, + updated_at: NaiveDateTime, + deleted_at: Option, +} + +/* +impl TestcaseResultRecord { + fn from_model(result: TestcaseResult) -> TestcaseResultRecord { + TestcaseResultRecord { + id: testcase.id.0, + problem_id: testcase.problem_id.0, + name: testcase.name, + input: testcase.input, + output: testcase.output, + explanation: testcase.explanation, + created_at: testcase.created_at, + updated_at: testcase.updated_at, + deleted_at: testcase.deleted_at, + } + } + fn to_model(&self) -> TestcaseResult { + Testcase { + id: TestcaseId(self.id), + problem_id: ProblemId(self.problem_id), + name: self.name, + input: self.input, + output: self.output, + explanation: self.explanation, + created_at: self.created_at, + updated_at: self.updated_at, + deleted_at: self.deleted_at, + } + } +} +*/ +#[async_trait] +pub trait ITestcaseResultRepository { + fn new(db: DBConnector) -> Self; + async fn logical_delete(&self, submit_id: &SubmitId) -> Result<(), GeneralError>; +} + +#[async_trait] +impl ITestcaseResultRepository for TestcasesResultRepository { + fn new(db: DBConnector) -> Self { + Self { db } + } + async fn logical_delete(&self, submit_id: &SubmitId) -> Result<(), GeneralError> { + self.db + .execute( + update( + testcase_results::table + .filter(testcase_results::submit_id.eq(submit_id.0)) + .filter(testcase_results::deleted_at.is_not_null()), + ) + .set(testcase_results::deleted_at.eq(Local::now().naive_local())), + ) + .await?; + Ok(()) + } +} diff --git a/src/repository.rs b/src/repository.rs deleted file mode 100644 index 5c52df4..0000000 --- a/src/repository.rs +++ /dev/null @@ -1,170 +0,0 @@ -use crate::entities::{Submit, Testcase}; -use crate::{db::DbPool, entities::Problem}; -use anyhow::Result; -use async_trait::async_trait; -use chrono::prelude::*; -use sqlx::{MySql, Transaction}; - -#[async_trait] -pub trait SubmitRepository { - async fn get_submits(&mut self) -> Result; - async fn update_status(&mut self, id: i64, status: &str) -> Result; -} - -#[async_trait] -impl SubmitRepository for Transaction<'_, MySql> { - async fn get_submits(&mut self) -> Result { - let submits = sqlx::query_as( - r#" - SELECT - id - , user_id - , problem_id - , path - , status - , point - , execution_time - , execution_memory - , compile_error - , lang - , created_at - , updated_at - , deleted_at - FROM - submits - WHERE - (status = 'WJ' OR status = 'WR') - AND deleted_at IS NULL - ORDER BY - updated_at ASC - LIMIT - 1 - FOR UPDATE - "#, - ) - .fetch_one(self) - .await?; - - Ok(submits) - } - - async fn update_status(&mut self, id: i64, status: &str) -> Result { - let result = sqlx::query!( - r#" - UPDATE submits - SET - status = ? - WHERE - id = ? - "#, - status, - id, - ) - .execute(self) - .await?; - Ok(result.rows_affected()) - } -} - -#[async_trait] -pub trait ProblemsRepository { - async fn fetch_problem(&self, problem_id: i64) -> Result; -} -#[async_trait] -impl ProblemsRepository for DbPool { - async fn fetch_problem(&self, problem_id: i64) -> Result { - let problems = sqlx::query_as!( - Problem, - r#" - SELECT - id - , slug - , name - , contest_id - , writer_user_id - , position - , uuid - , difficulty - , `statement` - , `constraints` - , input_format - , output_format - , created_at - , updated_at - , deleted_at - , checker_path - , execution_time_limit - FROM - problems - WHERE - id = ? - AND deleted_at IS NULL - "#, - problem_id, - ) - .fetch_one(self) - .await?; - Ok(problems) - } -} - -#[async_trait] -pub trait TestcasesRepository { - async fn fetch_testcases(&self, problem_id: i64) -> Result>; -} -#[async_trait] -impl TestcasesRepository for DbPool { - async fn fetch_testcases(&self, problem_id: i64) -> Result> { - let testcases = sqlx::query_as!( - Testcase, - r#" - SELECT - id - , problem_id - , name - , input - , output - , explanation - , created_at - , updated_at - , deleted_at - FROM - testcases - WHERE - problem_id = ? - AND deleted_at IS NULL - "#, - problem_id, - ) - .fetch_all(self) - .await?; - Ok(testcases) - } -} - -#[async_trait] -pub trait TestcaseResultsRepository { - async fn delete_testcase_results(&self, submit_id: i64) -> Result<()>; -} -#[async_trait] -impl TestcaseResultsRepository for DbPool { - async fn delete_testcase_results(&self, submit_id: i64) -> Result<()> { - sqlx::query( - r#" - UPDATE - testcase_results - SET - deleted_at = ? - WHERE - submit_id = ? - AND deleted_at IS NULL - "#, - ) - .bind(Local::now().naive_local()) - .bind(submit_id) - .execute(self) - .await?; - - Ok(()) - } -} diff --git a/src/schema.rs b/src/schema.rs new file mode 100644 index 0000000..bb9672e --- /dev/null +++ b/src/schema.rs @@ -0,0 +1,232 @@ +table! { + ar_internal_metadata (key) { + key -> Varchar, + value -> Nullable, + created_at -> Datetime, + updated_at -> Datetime, + } +} + +table! { + clarifications (id) { + id -> Bigint, + contest_id -> Bigint, + problem_id -> Nullable, + user_id -> Bigint, + question -> Varchar, + answer -> Nullable, + publish -> Bool, + created_at -> Datetime, + updated_at -> Datetime, + deleted_at -> Nullable, + } +} + +table! { + contests (id) { + id -> Bigint, + slug -> Varchar, + name -> Varchar, + description -> Nullable, + kind -> Varchar, + penalty_time -> Integer, + start_at -> Nullable, + end_at -> Nullable, + editorial_url -> Nullable, + created_at -> Datetime, + updated_at -> Datetime, + deleted_at -> Nullable, + } +} + +table! { + posts (id) { + id -> Bigint, + title -> Varchar, + content -> Text, + public_status -> Nullable, + created_at -> Datetime, + updated_at -> Datetime, + deleted_at -> Nullable, + } +} + +table! { + problems (id) { + id -> Bigint, + slug -> Nullable, + name -> Nullable, + contest_id -> Nullable, + writer_user_id -> Bigint, + position -> Nullable, + uuid -> Nullable, + difficulty -> Varchar, + execution_time_limit -> Integer, + statement -> Varchar, + constraints -> Varchar, + input_format -> Varchar, + output_format -> Varchar, + checker_path -> Nullable, + created_at -> Datetime, + updated_at -> Datetime, + deleted_at -> Nullable, + } +} + +table! { + registrations (id) { + id -> Bigint, + user_id -> Bigint, + contest_id -> Bigint, + created_at -> Datetime, + updated_at -> Datetime, + deleted_at -> Nullable, + } +} + +table! { + schema_migrations (version) { + version -> Varchar, + } +} + +table! { + submits (id) { + id -> Bigint, + user_id -> Integer, + problem_id -> Bigint, + path -> Varchar, + status -> Varchar, + point -> Nullable, + execution_time -> Nullable, + execution_memory -> Nullable, + compile_error -> Nullable, + lang -> Varchar, + created_at -> Datetime, + updated_at -> Datetime, + deleted_at -> Nullable, + } +} + +table! { + testcases (id) { + id -> Bigint, + problem_id -> Bigint, + name -> Nullable, + input -> Nullable, + output -> Nullable, + explanation -> Nullable, + created_at -> Datetime, + updated_at -> Datetime, + deleted_at -> Nullable, + } +} + +table! { + testcase_results (id) { + id -> Bigint, + submit_id -> Bigint, + testcase_id -> Bigint, + status -> Varchar, + execution_time -> Integer, + execution_memory -> Integer, + created_at -> Datetime, + updated_at -> Datetime, + deleted_at -> Nullable, + } +} + +table! { + testcase_sets (id) { + id -> Bigint, + problem_id -> Bigint, + name -> Varchar, + points -> Integer, + is_sample -> Bool, + created_at -> Datetime, + updated_at -> Datetime, + deleted_at -> Nullable, + } +} + +table! { + testcase_testcase_sets (id) { + id -> Bigint, + testcase_id -> Bigint, + testcase_set_id -> Bigint, + created_at -> Datetime, + updated_at -> Datetime, + deleted_at -> Nullable, + } +} + +table! { + tester_relations (id) { + id -> Bigint, + problem_id -> Bigint, + tester_user_id -> Bigint, + approved -> Bool, + created_at -> Datetime, + updated_at -> Datetime, + deleted_at -> Nullable, + } +} + +table! { + users (id) { + id -> Bigint, + provider -> Varchar, + uid -> Varchar, + encrypted_password -> Varchar, + reset_password_token -> Nullable, + reset_password_sent_at -> Nullable, + allow_password_change -> Nullable, + remember_created_at -> Nullable, + sign_in_count -> Integer, + current_sign_in_at -> Nullable, + last_sign_in_at -> Nullable, + current_sign_in_ip -> Nullable, + last_sign_in_ip -> Nullable, + confirmation_token -> Nullable, + confirmed_at -> Nullable, + confirmation_sent_at -> Nullable, + unconfirmed_email -> Nullable, + role -> Varchar, + name -> Nullable, + atcoder_id -> Nullable, + atcoder_rating -> Nullable, + writer_request_code -> Nullable, + email -> Nullable, + tokens -> Nullable, + created_at -> Datetime, + updated_at -> Datetime, + deleted_at -> Nullable, + } +} + +joinable!(problems -> contests (contest_id)); +joinable!(problems -> users (writer_user_id)); +joinable!(submits -> problems (problem_id)); +joinable!(testcase_sets -> problems (problem_id)); +joinable!(testcase_testcase_sets -> testcase_sets (testcase_set_id)); +joinable!(testcase_testcase_sets -> testcases (testcase_id)); +joinable!(testcases -> problems (problem_id)); +joinable!(tester_relations -> problems (problem_id)); +joinable!(tester_relations -> users (tester_user_id)); + +allow_tables_to_appear_in_same_query!( + ar_internal_metadata, + clarifications, + contests, + posts, + problems, + registrations, + schema_migrations, + submits, + testcases, + testcase_results, + testcase_sets, + testcase_testcase_sets, + tester_relations, + users, +); diff --git a/src/task.rs b/src/task.rs deleted file mode 100644 index 2bd898a..0000000 --- a/src/task.rs +++ /dev/null @@ -1,350 +0,0 @@ -use crate::{ - config::ENV_CONFIG, - db::DbPool, - entities, - lang_cmd::LANG_CMD, - models::container::{ - CompileRequest, CompileResponse, DownloadRequest, JudgeRequest, JudgeResponse, Problem, - Testcase, - }, - repository::{ - ProblemsRepository, SubmitRepository, TestcaseResultsRepository, TestcasesRepository, - }, - utils, -}; - -use anyhow::{bail, Result}; -use bollard::{ - container::{Config, CreateContainerOptions, RemoveContainerOptions, StartContainerOptions}, - models::HostConfig, - service::ContainerCreateResponse, - Docker, -}; -use futures::{ - future::FutureExt, - stream::{self, StreamExt}, -}; -use reqwest::{header, Client, Response, StatusCode}; -use std::{sync::Arc, time::Duration}; -use tokio::time::sleep; -// submit が取得できなかったときの次の取得までの間隔 -const INTERVAL: Duration = Duration::from_secs(1); -const MEM_LIMIT: i32 = 1_024_000; // 1,024,000KB(1,024KB) - -pub async fn gen_job(db_conn: Arc, docker_conn: Arc, http_client: Client) { - // この `task` が 1 実行単位 - let task = || { - let db_conn = Arc::clone(&db_conn); - let docker_conn = Arc::clone(&docker_conn); - let http_client = http_client.clone(); - async move { - let task = JudgeTask::new(db_conn, docker_conn, http_client); - // 提出を取得 - let submit = match task.fetch_submit().await { - Ok(s) => s, - Err(_) => { - // TODO(magurotuna): 提出が取得できなかった場合は 1 秒待って次の実行に移る - sleep(INTERVAL).await; - bail!("Couldn't find an unjudged submit."); - } - }; - // TODO(magurotuna): コンテナ名をちゃんとする UUIDを発行? - let container_name = format!("DUMMY_NAME_{}", utils::gen_rand_string(6)); - - match execute_task(&task, &submit, &container_name).await { - Ok(()) => (), - Err(e) => { - eprint!("{}", e); - sleep(INTERVAL).await; - task.save_internal_error(submit.id).await?; - //task.remove_container(&container_name).await?; - bail!("internal error"); - } - } - Ok(()) - } - }; - - // stream::unfold をすることで、1 実行単位である `task` を延々と繰り返すような Stream を作る - let mut stream = stream::unfold((), move |_| { - // カッコが続いて見づらくなるので Unit に置き換えて多少見やすくなるようにしている - - const UNIT: () = (); - task().map(|task_result| Some((task_result, UNIT))) - }) - .boxed(); - - while let Some(_task_result) = stream.next().await { - // 1回1回の task の実行結果を使って何かやりたければここに書く - // ログ出力とか? - } -} -async fn execute_task( - task: &JudgeTask, - submit: &entities::Submit, - container_name: &str, -) -> Result<()> { - let command = match LANG_CMD.get(&submit.lang) { - Some(command) => command, - None => { - // statusをIEへ - task.save_internal_error(submit.id).await?; - - bail!("Couldn't find a language command setting."); - } - }; - - // リジャッジなら過去の testcase_results をすべて消す。 - if submit.status == "WR" { - task.delete_testcase_results(submit.id).await?; - } - - // コンテナ作成 - - let (_container, ip_addr) = task.create_container(&container_name).await?; - // テストケース、問題を取得 - - let problem = task.fetch_problem(submit.problem_id).await?; - let testcases = task.fetch_testcases(submit.problem_id).await?; - - let req = generate_judge_request(submit.id, &command.run, problem, testcases); - - let download_response = task - .request_download( - &ip_addr, - &DownloadRequest { - submit_id: submit.id, - code_path: submit.path.clone(), - filename: command.file_name.clone(), - }, - ) - .await?; - - if download_response.status() != StatusCode::OK { - return Err(anyhow::anyhow!("Download failed")); - } - - let compile_response = task - .request_compile( - &ip_addr, - &CompileRequest { - submit_id: submit.id, - cmd: command.compile.clone(), - }, - ) - .await?; - // コンパイルエラーはコンテナの中で処理をしているはずなので ok - if !compile_response.0.ok { - return Ok(()); - } - - let _judge_response = task.request_judge(&ip_addr, &req).await?; - // TODO judgeレスポンスによる処理 - // コンテナを削除 - task.remove_container(&container_name).await?; - - Ok(()) -} -fn generate_judge_request( - submit_id: i64, - cmd: &str, - problem: entities::Problem, - testcases: Vec, -) -> JudgeRequest { - let request_testcases = testcases - .iter() - .map(|t| Testcase { - testcase_id: t.id, - name: t.name.clone().unwrap_or_default(), - }) - .collect(); - let request_problem = Problem { - problem_id: problem.id, - uuid: problem.uuid.unwrap_or_default(), - checker_path: problem - .checker_path - .unwrap_or_else(|| "checker_path/wcmp.cpp".to_string()), - }; - JudgeRequest { - submit_id, - cmd: cmd.to_string(), - time_limit: problem.execution_time_limit, - mem_limit: MEM_LIMIT, - testcases: request_testcases, - problem: request_problem, - } -} - -/// 1つの submit に対するジャッジの処理を担当する -#[derive(Debug)] -struct JudgeTask { - db_conn: Arc, - docker_conn: Arc, - http_client: Client, -} - -impl JudgeTask { - fn new(db_conn: Arc, docker_conn: Arc, http_client: Client) -> Self { - Self { - db_conn, - docker_conn, - http_client, - } - } - - /// 未ジャッジの提出のうち、もっとも古いもの1件を取得する。 - /// その1件のステータスを「ジャッジ中」にする - async fn fetch_submit(&self) -> Result { - let mut conn = self.db_conn.begin().await?; - let submit = conn.get_submits().await?; - conn.update_status(submit.id, "WIP").await?; - conn.commit().await?; - Ok(submit) - } - async fn fetch_problem(&self, problem_id: i64) -> Result { - Ok(self.db_conn.fetch_problem(problem_id).await?) - } - - async fn fetch_testcases(&self, problem_id: i64) -> Result> { - Ok(self.db_conn.fetch_testcases(problem_id).await?) - } - - async fn delete_testcase_results(&self, submit_id: i64) -> Result<()> { - Ok(self.db_conn.delete_testcase_results(submit_id).await?) - } - - /// Docker コンテナを指定された名前で立ち上げる - async fn create_container(&self, name: &str) -> Result<(ContainerCreateResponse, String)> { - const IMAGE: &str = "cafecoder_docker:2104"; - - let options = Some(CreateContainerOptions { name }); - let config = Config { - image: Some(IMAGE), - host_config: Some(HostConfig { - memory: Some(2_147_483_648_i64), - pids_limit: Some(512_i64), - privileged: Some(true), - ..Default::default() - }), - ..Default::default() - }; - let res = self.docker_conn.create_container(options, config).await?; - - self.docker_conn - .start_container(name, None::>) - .await?; - let inspect = self.docker_conn.inspect_container(name, None).await?; - - let network_settings = inspect - .network_settings - .expect("couldn't get network_settings"); - let ip_addr = network_settings - .ip_address - .expect("couldn't get IP address"); - - // TODO: コンテナが立ち上がったかどうかのチェック - tokio::time::sleep(Duration::new(1, 0)).await; - - Ok((res, ip_addr)) - } - - pub async fn request_compile( - &self, - ip_addr: &str, - req: &CompileRequest, - ) -> Result { - dbg!(serde_json::to_string(req).unwrap()); - let resp = self - .http_client - .post(&format!( - "http://{}:{}/compile", - &ip_addr, &ENV_CONFIG.judge_container_port - )) - .json(&req) - .header(header::CONTENT_TYPE, "application/json") - .send() - .await?; - - if resp.status() != StatusCode::OK { - anyhow::bail!(format!( - "{}\n{}", - "compile: response status code was not 200 OK", - resp.text().await? - )); - } - - let resp = resp.json().await?; - - Ok(resp) - } - - pub async fn request_download( - &self, - ip_addr: &str, - req: &DownloadRequest, - ) -> Result { - dbg!(serde_json::to_string(req).unwrap()); - let resp = self - .http_client - .post(&format!( - "http://{}:{}/download", - &ip_addr, &ENV_CONFIG.judge_container_port - )) - .json(&req) - .header(header::CONTENT_TYPE, "application/json") - .send() - .await?; - - Ok(resp) - } - pub async fn request_judge( - &self, - ip_addr: &str, - req: &JudgeRequest, - ) -> Result { - dbg!(serde_json::to_string(req).unwrap()); - let resp = self - .http_client - .post(&format!( - "http://{}:{}/judge", - &ip_addr, &ENV_CONFIG.judge_container_port - )) - .header(header::CONTENT_TYPE, "application/json") - .json(&req) - .send() - .await?; - - if resp.status() != StatusCode::OK { - bail!(format!( - "judge: response status code was not 200 OK\n{}", - resp.text().await? - )); - } - - Ok(resp.json().await?) - } - - /// Docker コンテナを削除する - async fn remove_container(&self, name: &str) -> Result<()> { - let options = RemoveContainerOptions { - force: true, - ..Default::default() - }; - self.docker_conn - .remove_container(name, Some(options)) - .await?; - Ok(()) - } - - async fn save_internal_error(&self, submit_id: i64) -> Result<()> { - let mut conn = self.db_conn.begin().await?; - let row = conn.update_status(submit_id, "IE").await?; - if row == 1 { - conn.commit().await?; - - Ok(()) - } else { - Err(anyhow::anyhow!("Update failed")) - } - } -} diff --git a/src/wrapper.rs b/src/wrapper.rs new file mode 100644 index 0000000..954dc9f --- /dev/null +++ b/src/wrapper.rs @@ -0,0 +1,2 @@ +pub mod error; +pub use error::*; diff --git a/src/wrapper/error.rs b/src/wrapper/error.rs new file mode 100644 index 0000000..9ce9b52 --- /dev/null +++ b/src/wrapper/error.rs @@ -0,0 +1,84 @@ +use anyhow::Error; +use std::any::Any; + +pub trait IGeneralError: Any { + fn error_type(&self) -> String { + "internal_server_error".to_string() + } +} + +#[derive(Debug)] +pub struct GeneralError { + type_id: std::any::TypeId, + error_type: String, + inner: Error, +} + +impl GeneralError { + pub fn new(err: impl IGeneralError, detail: E) -> GeneralError + where + Error: From, + { + GeneralError { + type_id: err.type_id(), + error_type: err.error_type(), + inner: From::from(detail), + } + } + + pub fn into_inner(self) -> Error { + self.inner + } + + pub fn error_type(&self) -> String { + self.error_type.clone() + } + + pub fn is_error_of(&self, err: impl IGeneralError) -> bool { + self.type_id == err.type_id() && self.error_type() == err.error_type() + } + pub fn only(msg: &'static str) -> GeneralError { + GeneralError { + type_id: std::any::TypeId::of::(), + error_type: String::from(msg), + inner: Error::msg(msg.clone()), + } + } +} + +// failure::Error can be treated as GeneralError +impl IGeneralError for Error {} + +pub enum FutureError { + JoinError, +} +pub enum DockerError { + InternalError, +} + +// for tokio::task::spawn_blocking +impl IGeneralError for FutureError { + fn error_type(&self) -> String { + match self { + FutureError::JoinError => "internal_server_error".to_string(), + } + } +} +impl IGeneralError for DockerError { + fn error_type(&self) -> String { + match self { + DockerError::InternalError => "internal_docker_error".to_string(), + } + } +} +impl IGeneralError for bollard::errors::Error { + fn error_type(&self) -> String { + "docker_error".to_string() + } +} + +impl From for GeneralError { + fn from(err: tokio::task::JoinError) -> GeneralError { + GeneralError::new(FutureError::JoinError, err) + } +}