diff --git a/Cargo.lock b/Cargo.lock index 9a06f10..dc36df8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -554,18 +554,18 @@ dependencies = [ [[package]] name = "cranelift-bforest" -version = "0.112.1" +version = "0.112.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6e376bd92bddd03dcfc443b14382611cae5d10012aa0b1628bbf18bb73f12f7" +checksum = "7b765ed4349e66bedd9b88c7691da42e24c7f62067a6be17ddffa949367b6e17" dependencies = [ "cranelift-entity", ] [[package]] name = "cranelift-bitset" -version = "0.112.1" +version = "0.112.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45ecbe07f25a8100e5077933516200e97808f1d7196b5a073edb85fa08fde32e" +checksum = "9eaa2aece6237198afd32bff57699e08d4dccb8d3902c214fc1e6ba907247ca4" dependencies = [ "serde", "serde_derive", @@ -573,9 +573,9 @@ dependencies = [ [[package]] name = "cranelift-codegen" -version = "0.112.1" +version = "0.112.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc60913f32c1de18538c28bef74b8c87cf16de7841a1b0956fcf01b23237853a" +checksum = "351824439e59d42f0e4fa5aac1d13deded155120043565769e55cd4ad3ca8ed9" dependencies = [ "bumpalo", "cranelift-bforest", @@ -596,33 +596,33 @@ dependencies = [ [[package]] name = "cranelift-codegen-meta" -version = "0.112.1" +version = "0.112.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bae009e7822f47aa55e7dcef846ccf3aa4eb102ca6b4bcb8a44b36f3f49aa85c" +checksum = "5a0ce0273d7a493ef8f31f606849a4e931c19187a4923f5f87fc1f2b13109981" dependencies = [ "cranelift-codegen-shared", ] [[package]] name = "cranelift-codegen-shared" -version = "0.112.1" +version = "0.112.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c78f01a852536c68e34444450f845ed6e0782a1f047f85397fe460b8fbce8f1" +checksum = "0f72016ac35579051913f4f07f6b36c509ed69412d852fd44c8e1d7b7fa6d92a" [[package]] name = "cranelift-control" -version = "0.112.1" +version = "0.112.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a061b22e00a9e36b31f2660dfb05a9617b7775bd54b79754d3bb75a990dac06" +checksum = "db28951d21512c4fd0554ef179bfb11e4eb6815062957a9173824eee5de0c46c" dependencies = [ "arbitrary", ] [[package]] name = "cranelift-entity" -version = "0.112.1" +version = "0.112.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95e2b261a3e74ae42f4e606906d5ffa44ee2684e8b1ae23bdf75d21908dc9233" +checksum = "14ebe592a2f81af9237cf9be29dd3854ecb72108cfffa59e85ef12389bf939e3" dependencies = [ "cranelift-bitset", "serde", @@ -631,9 +631,9 @@ dependencies = [ [[package]] name = "cranelift-frontend" -version = "0.112.1" +version = "0.112.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe14abba0e6bab42aca0f9ce757f96880f9187e88bc6cb975ed6acd8a42f7770" +checksum = "4437db9d60c7053ac91ded0802740c2ccf123ee6d6898dd906c34f8c530cd119" dependencies = [ "cranelift-codegen", "log", @@ -643,15 +643,15 @@ dependencies = [ [[package]] name = "cranelift-isle" -version = "0.112.1" +version = "0.112.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "311d91ae72b37d4262b51217baf8c9e01f1afd5148931468da1fdb7e9d011347" +checksum = "230cb33572b9926e210f2ca28145f2bc87f389e1456560932168e2591feb65c1" [[package]] name = "cranelift-native" -version = "0.112.1" +version = "0.112.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a3f84c75e578189ff7a716c24ad83740b553bf583f2510b323bfe4c1a74bb93" +checksum = "364524ac7aef7070b1141478724abebeec297d4ea1e87ad8b8986465e91146d9" dependencies = [ "cranelift-codegen", "libc", @@ -660,9 +660,9 @@ dependencies = [ [[package]] name = "cranelift-wasm" -version = "0.112.1" +version = "0.112.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f56b7b2476c47b2091eee5a20bc54a80fbb29ca5313ae2bd0dea52621abcfca1" +checksum = "0572cbd9d136a62c0f39837b6bce3b0978b96b8586794042bec0c214668fd6f5" dependencies = [ "cranelift-codegen", "cranelift-entity", @@ -1478,7 +1478,7 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "lyric" -version = "0.1.3" +version = "0.1.4-rc0" dependencies = [ "async-trait", "bollard", @@ -1507,7 +1507,7 @@ dependencies = [ [[package]] name = "lyric-py" -version = "0.1.3" +version = "0.1.4-rc0" dependencies = [ "anyhow", "async-trait", @@ -1533,7 +1533,7 @@ dependencies = [ [[package]] name = "lyric-rpc" -version = "0.1.3" +version = "0.1.4-rc0" dependencies = [ "async-trait", "lyric-utils", @@ -1546,9 +1546,10 @@ dependencies = [ [[package]] name = "lyric-utils" -version = "0.1.3" +version = "0.1.4-rc0" dependencies = [ "anyhow", + "bitflags", "chrono", "lazy_static", "local-ip-address", @@ -1562,7 +1563,7 @@ dependencies = [ [[package]] name = "lyric-wasm-runtime" -version = "0.1.3" +version = "0.1.4-rc0" dependencies = [ "anyhow", "async-stream", @@ -3033,9 +3034,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasi-common" -version = "25.0.1" +version = "25.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4738c7ff6d16fa9f21efa1982a23d428072a930a50d7f1dcb2638e36a0a52085" +checksum = "5f1e63f999ecfdd96d64d35b39d0577318d9d2eae2d41603d4befda3b3dfe252" dependencies = [ "anyhow", "bitflags", @@ -3245,9 +3246,9 @@ dependencies = [ [[package]] name = "wasmtime" -version = "25.0.1" +version = "25.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03601559991d459a228236a49135364eac85ac00dc07b65fb95ae61a957793af" +checksum = "ef01f9cb9636ed42a7ec5a09d785c0643590199dc7372dc22c7e2ba7a31a97d4" dependencies = [ "addr2line 0.22.0", "anyhow", @@ -3301,18 +3302,18 @@ dependencies = [ [[package]] name = "wasmtime-asm-macros" -version = "25.0.1" +version = "25.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e453b3bde07312874c0c6703e2de9281daab46646172c1b71fa59a97226f858e" +checksum = "ba5b20797419d6baf2296db2354f864e8bb3447cacca9d151ce7700ae08b4460" dependencies = [ "cfg-if", ] [[package]] name = "wasmtime-cache" -version = "25.0.1" +version = "25.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a35e1d7cce7b536cc71955e5898b099104a577d2583694b7b31a6f38c14c04a3" +checksum = "272d5939e989c5b54e3fa83ef420e4a6dba3995c3065626066428b2f73ad1e06" dependencies = [ "anyhow", "base64 0.21.7", @@ -3330,9 +3331,9 @@ dependencies = [ [[package]] name = "wasmtime-component-macro" -version = "25.0.1" +version = "25.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a6faeabbdbfd27e24e8d5204207ba9c247a13cf84181ea721b5f209f281fe01" +checksum = "26593c4b18c76ca3c3fbdd813d6692256537b639b851d8a6fe827e3d6966fc01" dependencies = [ "anyhow", "proc-macro2", @@ -3345,15 +3346,15 @@ dependencies = [ [[package]] name = "wasmtime-component-util" -version = "25.0.1" +version = "25.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b1b24db4aa3dc7c0d3181d1833b4fe9ec0cd3f08780b746415c84c0a9ec9011" +checksum = "a2ed562fbb0cbed20a56c369c8de146c1de06a48c19e26ed9aa45f073514ee60" [[package]] name = "wasmtime-cranelift" -version = "25.0.1" +version = "25.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c737bef9ea94aab874e29ac6a8688b89ceb43c7b51f047079c43387972c07ee3" +checksum = "f389b789cbcb53a8499131182135dea21d7d97ad77e7fb66830f69479ef0e68c" dependencies = [ "anyhow", "cfg-if", @@ -3376,9 +3377,9 @@ dependencies = [ [[package]] name = "wasmtime-environ" -version = "25.0.1" +version = "25.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "817bfa9ea878ec37aa24f85fd6912844e8d87d321662824cf920d561b698cdfd" +checksum = "84b72debe8899f19bedf66f7071310f06ef62de943a1369ba9b373613e77dd3d" dependencies = [ "anyhow", "cpp_demangle", @@ -3403,9 +3404,9 @@ dependencies = [ [[package]] name = "wasmtime-fiber" -version = "25.0.1" +version = "25.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5070971b479b4e4879dbae8a8e1efee738a36d047c5738acfedb38d6740b79d1" +checksum = "92b8d4d504266ee598204f9e69cea8714499cc7c5aeddaa9b3f76aaace8b0680" dependencies = [ "anyhow", "cc", @@ -3418,9 +3419,9 @@ dependencies = [ [[package]] name = "wasmtime-jit-debug" -version = "25.0.1" +version = "25.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26fd0000903068c13465b9c023f56f0664f433035cbbd8eae69aa7c755f97637" +checksum = "48ed7f0bbb9da3252c252b05fcd5fd42672db161e6276aa96e92059500247d8c" dependencies = [ "object", "once_cell", @@ -3430,9 +3431,9 @@ dependencies = [ [[package]] name = "wasmtime-jit-icache-coherence" -version = "25.0.1" +version = "25.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48011232c0da424f89c3752a378d0b7f512fae321ea414a43e1e7a302a6a1f7e" +checksum = "1d930bc1325bc0448be6a11754156d770f56f6c3a61f440e9567f36cd2ea3065" dependencies = [ "anyhow", "cfg-if", @@ -3442,15 +3443,15 @@ dependencies = [ [[package]] name = "wasmtime-slab" -version = "25.0.1" +version = "25.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9858a22e656ae8574631221b474b8bebf63f1367fcac3f179873833eabc2ced" +checksum = "055a181b8d03998511294faea14798df436503f14d7fd20edcf7370ec583e80a" [[package]] name = "wasmtime-types" -version = "25.0.1" +version = "25.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d14b8a9206fe94485a03edb1654cd530dbd2a859a85a43502cb4e99653a568c" +checksum = "c8340d976673ac3fdacac781f2afdc4933920c1adc738c3409e825dab3955399" dependencies = [ "anyhow", "cranelift-entity", @@ -3462,9 +3463,9 @@ dependencies = [ [[package]] name = "wasmtime-versioned-export-macros" -version = "25.0.1" +version = "25.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9bb1f01efb8b542eadfda511e8ea1cc54309451aba97b69969e5b1a59cb7ded" +checksum = "a4b0c1f76891f778db9602ee3fbb4eb7e9a3f511847d1fb1b69eddbcea28303c" dependencies = [ "proc-macro2", "quote", @@ -3473,9 +3474,9 @@ dependencies = [ [[package]] name = "wasmtime-wasi" -version = "25.0.1" +version = "25.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cc26a3923937186f5a4a4fa44d9dd1a7a389bbb06f8bac03cde2d52aad3ace8" +checksum = "ba1497b38341acc97308d6ce784598419fe0131bf6ddc5cda16a91033ef7c66e" dependencies = [ "anyhow", "async-trait", @@ -3527,9 +3528,9 @@ dependencies = [ [[package]] name = "wasmtime-winch" -version = "25.0.1" +version = "25.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b238eeaf55652df0e63a6829d1ca9ef726d63517f56194faa0f6b9941f8d9151" +checksum = "a702ff5eff3b37c11453ec8b54ec444bb9f2c689c7a7af382766c52df86b1e9b" dependencies = [ "anyhow", "cranelift-codegen", @@ -3544,9 +3545,9 @@ dependencies = [ [[package]] name = "wasmtime-wit-bindgen" -version = "25.0.1" +version = "25.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1596caa67b31ac675fd3da61685c4260f8b10832021db42c85d227b7ba8133" +checksum = "b2fca2cbb5bb390f65d4434c19bf8d9873dfc60f10802918ebcd6f819a38d703" dependencies = [ "anyhow", "heck 0.4.1", @@ -3596,9 +3597,9 @@ dependencies = [ [[package]] name = "wiggle" -version = "25.0.1" +version = "25.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e998c11dd3f293a8f657ce14e9c8fdc5cddd858f1e6448290d4ec04ca7ffe5b7" +checksum = "e4ebee2be6b561d1fe91b37e960c02baa94cdee29af863f5f26a0637f344f27a" dependencies = [ "anyhow", "async-trait", @@ -3611,9 +3612,9 @@ dependencies = [ [[package]] name = "wiggle-generate" -version = "25.0.1" +version = "25.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0d8db385e5207c1ac431837868c8e48fc3f7e81f3a146793086d661875520ec" +checksum = "97c4a32959189041ccb260e6dfa7fcf907e665166e755a6a681c32423c90e45f" dependencies = [ "anyhow", "heck 0.4.1", @@ -3626,9 +3627,9 @@ dependencies = [ [[package]] name = "wiggle-macro" -version = "25.0.1" +version = "25.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2706ee9e7d1e106de80a3b3c6ec2f42920149e2def3e8060187c8e61f99bc0ef" +checksum = "6e1c266e16c4b24a29e055ec651e27fce1389c886bb00fbe78b8924a253a439b" dependencies = [ "proc-macro2", "quote", @@ -3669,9 +3670,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "winch-codegen" -version = "0.23.1" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac790aaeff15764481c731239a45346df3f0af966839ac1575f49989fdbb542" +checksum = "d716f7c87db8ea79f1dc69f7344354b6256451bccca422ac4c3e0d607d144532" dependencies = [ "anyhow", "cranelift-codegen", diff --git a/Cargo.toml b/Cargo.toml index e2c7d16..317cb9c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ default-members = [ ] [workspace.package] -version = "0.1.3" +version = "0.1.4-rc0" authors = ["fangyinc "] edition = "2021" homepage = "https://github.com/fangyinc/lyric" @@ -43,9 +43,10 @@ tokio-util = { version = "0.7.12", features = ["io"] } tokio-stream = { version = "0.1.16", features = ["io-util"] } pyo3 = { version = "0.22.5" } python3-dll-a = "0.2.10" +bitflags = "2.6" -lyric = { version = "0.1.3", path = "crates/lyric", default-features = false } -lyric-utils = { version = "0.1.3", path = "crates/lyric-utils", default-features = false } -lyric-rpc = { version = "0.1.3", path = "crates/lyric-rpc", default-features = false } -lyric-wasm-runtime = { version = "0.1.3", path = "crates/lyric-wasm-runtime", default-features = false } -lyric-py = { version = "0.1.3", path = "bindings/python/lyric-py", default-features = false } \ No newline at end of file +lyric = { version = "0.1.4-rc0", path = "crates/lyric", default-features = false } +lyric-utils = { version = "0.1.4-rc0", path = "crates/lyric-utils", default-features = false } +lyric-rpc = { version = "0.1.4-rc0", path = "crates/lyric-rpc", default-features = false } +lyric-wasm-runtime = { version = "0.1.4-rc0", path = "crates/lyric-wasm-runtime", default-features = false } +lyric-py = { version = "0.1.4-rc0", path = "bindings/python/lyric-py", default-features = false } \ No newline at end of file diff --git a/README.md b/README.md index e51461b..8d15921 100644 --- a/README.md +++ b/README.md @@ -21,19 +21,19 @@ A Rust-powered secure sandbox for multi-language code execution, leveraging WebA **Install Lyric via pip:** ```bash -pip install "lyric-py>=0.1.3" +pip install "lyric-py>=0.1.4-rc0" ``` **Install default Python webassembly worker:** ```bash -pip install "lyric-py-worker>=0.1.3" +pip install "lyric-py-worker>=0.1.4-rc0" ``` **Install default JavaScript webassembly worker:** ```bash -pip install "lyric-js-worker>=0.1.3" +pip install "lyric-js-worker>=0.1.4-rc0" ``` ### Basic Usage @@ -42,6 +42,17 @@ pip install "lyric-js-worker>=0.1.3" import asyncio from lyric import DefaultLyricDriver +python_code = """ +def add(a, b): + return a + b +result = add(1, 2) +print(result) +""" + +js_code = """ +console.log('Hello from JavaScript!'); +""" + async def main(): lcd = DefaultLyricDriver(host="localhost", log_level="ERROR") lcd.start() @@ -50,21 +61,10 @@ async def main(): await lcd.lyric.load_default_workers() # Execute Python code - python_code = """ - def add(a, b): - return a + b - result = add(1, 2) - print(result) - """ - py_res = await lcd.exec(python_code, "python") print(py_res) # Execute JavaScript code - js_code = """ - console.log('Hello from JavaScript!'); - """ - js_res = await lcd.exec(js_code, "javascript") print(js_res) @@ -81,13 +81,7 @@ import asyncio import json from lyric import DefaultLyricDriver -async def main(): - lcd = DefaultLyricDriver(host="localhost", log_level="ERROR") - lcd.start() - - # Load workers(default: Python, JavaScript) - await lcd.lyric.load_default_workers() - py_func = """ +py_func = """ def message_handler(message_dict): user_message = message_dict.get("user_message") ai_message = message_dict.get("ai_message") @@ -99,18 +93,8 @@ def message_handler(message_dict): "handler_language": "python", } """ - input_data = { - "user_message": "Hello from user", - "ai_message": "Hello from AI", - } - input_bytes = json.dumps(input_data).encode("utf-8") - py_res = await lcd.exec1(py_func, input_bytes, "message_handler", lang="python") - # Get the result of the function execution - result_dict = py_res.output - print("Python result:", result_dict) - print(f"Full output: {py_res}") - js_func = """ +js_func = """ function message_handler(message_dict) { return { user: message_dict.user_message, @@ -121,6 +105,25 @@ function message_handler(message_dict) { }; } """ +async def main(): + lcd = DefaultLyricDriver(host="localhost", log_level="ERROR") + lcd.start() + + # Load workers(default: Python, JavaScript) + await lcd.lyric.load_default_workers() + + input_data = { + "user_message": "Hello from user", + "ai_message": "Hello from AI", + } + input_bytes = json.dumps(input_data).encode("utf-8") + + py_res = await lcd.exec1(py_func, input_bytes, "message_handler", lang="python") + # Get the result of the function execution + result_dict = py_res.output + print("Python result:", result_dict) + print(f"Full output: {py_res}") + js_res = await lcd.exec1(js_func, input_bytes, "message_handler", lang="javascript") # Get the result of the function execution result_dict = js_res.output @@ -133,6 +136,59 @@ function message_handler(message_dict) { asyncio.run(main()) ``` +## Advanced Usage + +### Execution With Specific Resources + +```python +import asyncio +from lyric import DefaultLyricDriver, PyTaskResourceConfig, PyTaskFsConfig, PyTaskMemoryConfig + +lcd = DefaultLyricDriver(host="localhost", log_level="ERROR") +lcd.start() + +python_code = """ +import os + +# List the files in the root directory +root = os.listdir('/tmp/') +print("Files in the root directory:", root) + +# Create a new file in the home directory +with open('/home/new_file.txt', 'w') as f: + f.write('Hello, World!') +""" + +async def main(): + # Load workers(default: Python, JavaScript) + await lcd.lyric.load_default_workers() + + dir_read, dir_write = 1, 2 + file_read, file_write = 3, 4 + resources = PyTaskResourceConfig( + fs=PyTaskFsConfig( + preopens=[ + # Mount current directory in host to "/tmp" in the sandbox with read permission + (".", "/tmp", dir_read, file_read), + # Mount "/tmp" in host to "/home" in the sandbox with read and write permission + ("/tmp", "/home", dir_read | dir_write, file_read | file_write), + ] + ), + memory=PyTaskMemoryConfig( + # Set the memory limit to 30MB + memory_limit=30 * 1024 * 1024 # 30MB in bytes + ) + ) + + py_res = await lcd.exec(python_code, "python", resources=resources) + assert py_res.exit_code == 0, "Python code should exit with 0" + + # Stop the driver + lcd.stop() + +asyncio.run(main()) +``` + ## Examples - [Notebook-Qick Start](examples/notebook/lyric_quick_start.ipynb): A Jupyter notebook demonstrating how to use Lyric to execute Python and JavaScript code. diff --git a/bindings/javascript/lyric-js-worker/wit/deps/task/types.wit b/bindings/javascript/lyric-js-worker/wit/deps/task/types.wit index 1061a37..0f1ab9a 100644 --- a/bindings/javascript/lyric-js-worker/wit/deps/task/types.wit +++ b/bindings/javascript/lyric-js-worker/wit/deps/task/types.wit @@ -1,11 +1,92 @@ interface types { + + /// CPU Resource Configuration + record cpu-config { + /// CPU core limit (e.g. 1.0 represents a full core, 0.5 represents half a core) + cpu-cores: option, + /// CPU share value (similar to --cpu-shares in docker, default 1024) + cpu-shares: option, + /// CPU period configuration (microseconds, at most use quota microseconds within period) + cpu-quota: option, + /// CPU period time (microseconds, default 100000, i.e. 100ms) + cpu-period: option + } + + /// Memory Resource Configuration + record memory-config { + /// Memory limit in bytes + memory-limit: option + } + + /// Network Resource Configuration + record network-config { + /// Whether to enable network access + enable-network: bool, + /// Inbound bandwidth limit (KB/s) + ingress-limit-kbps: option, + /// Outbound bandwidth limit (KB/s) + egress-limit-kbps: option, + /// Allowed host list + allowed-hosts: option>, + /// Allowed port range list (start_port, end_port) + allowed-ports: option>> + } + + /// File system permission + flags file-perms { + /// Read permission + read, + /// Write permission + write, + } + + /// File system configuration + record fs-config { + /// Pre-mapped directory list (host path, container path, directory permissions, file permissions) + preopens: list>, + /// File system size limit in bytes + fs-size-limit: option, + /// Temporary directory for wasi + temp-dir: option + } + + /// Instance limits + record instance-limits { + // Max number of instances + max-instances: option, + /// Max number of tables + max-tables: option, + /// Max number of elements in each table + max-table-elements: option + } + + /// Full resource configuration + record resource-config { + /// CPU configuration + cpu: option, + /// Memory configuration + memory: option, + /// Network configuration + network: option, + /// File system configuration + fs: option, + // Instance limits + instance: option, + /// Task timeout in milliseconds + timeout-ms: option, + /// List of environment variables + env-vars: list> + } + /// A request to interpret a script record interpreter-request { + /// Resource configuration + resources: option, /// The protocol version of the interpreter protocol: u32, /// The language of the script lang: string, - /// The script to be interpreted + /// The script to be interpreted code: string } @@ -29,6 +110,8 @@ interface types { } record binary-request { + /// Resource configuration + resources: option, protocol: u32, data: list } diff --git a/bindings/python/lyric-js-worker/pyproject.toml b/bindings/python/lyric-js-worker/pyproject.toml index 00661ce..b8aa9dd 100644 --- a/bindings/python/lyric-js-worker/pyproject.toml +++ b/bindings/python/lyric-js-worker/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lyric-js-worker" -version = "0.1.3" +version = "0.1.4-rc0" description = "Add your description here" authors = [ { name = "Fangyin Cheng", email = "staneyffer@gmail.com" }, diff --git a/bindings/python/lyric-py-worker/pyproject.toml b/bindings/python/lyric-py-worker/pyproject.toml index 5e030b3..6dcbe6c 100644 --- a/bindings/python/lyric-py-worker/pyproject.toml +++ b/bindings/python/lyric-py-worker/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lyric-py-worker" -version = "0.1.3" +version = "0.1.4-rc0" description = "Add your description here" authors = [ { name = "Fangyin Cheng", email = "staneyffer@gmail.com" }, diff --git a/bindings/python/lyric-py-worker/src/wit/deps/task/types.wit b/bindings/python/lyric-py-worker/src/wit/deps/task/types.wit index 1061a37..0f1ab9a 100644 --- a/bindings/python/lyric-py-worker/src/wit/deps/task/types.wit +++ b/bindings/python/lyric-py-worker/src/wit/deps/task/types.wit @@ -1,11 +1,92 @@ interface types { + + /// CPU Resource Configuration + record cpu-config { + /// CPU core limit (e.g. 1.0 represents a full core, 0.5 represents half a core) + cpu-cores: option, + /// CPU share value (similar to --cpu-shares in docker, default 1024) + cpu-shares: option, + /// CPU period configuration (microseconds, at most use quota microseconds within period) + cpu-quota: option, + /// CPU period time (microseconds, default 100000, i.e. 100ms) + cpu-period: option + } + + /// Memory Resource Configuration + record memory-config { + /// Memory limit in bytes + memory-limit: option + } + + /// Network Resource Configuration + record network-config { + /// Whether to enable network access + enable-network: bool, + /// Inbound bandwidth limit (KB/s) + ingress-limit-kbps: option, + /// Outbound bandwidth limit (KB/s) + egress-limit-kbps: option, + /// Allowed host list + allowed-hosts: option>, + /// Allowed port range list (start_port, end_port) + allowed-ports: option>> + } + + /// File system permission + flags file-perms { + /// Read permission + read, + /// Write permission + write, + } + + /// File system configuration + record fs-config { + /// Pre-mapped directory list (host path, container path, directory permissions, file permissions) + preopens: list>, + /// File system size limit in bytes + fs-size-limit: option, + /// Temporary directory for wasi + temp-dir: option + } + + /// Instance limits + record instance-limits { + // Max number of instances + max-instances: option, + /// Max number of tables + max-tables: option, + /// Max number of elements in each table + max-table-elements: option + } + + /// Full resource configuration + record resource-config { + /// CPU configuration + cpu: option, + /// Memory configuration + memory: option, + /// Network configuration + network: option, + /// File system configuration + fs: option, + // Instance limits + instance: option, + /// Task timeout in milliseconds + timeout-ms: option, + /// List of environment variables + env-vars: list> + } + /// A request to interpret a script record interpreter-request { + /// Resource configuration + resources: option, /// The protocol version of the interpreter protocol: u32, /// The language of the script lang: string, - /// The script to be interpreted + /// The script to be interpreted code: string } @@ -29,6 +110,8 @@ interface types { } record binary-request { + /// Resource configuration + resources: option, protocol: u32, data: list } diff --git a/bindings/python/lyric-py-worker/src/worker.py b/bindings/python/lyric-py-worker/src/worker.py index 1d3c1ff..ac520ff 100644 --- a/bindings/python/lyric-py-worker/src/worker.py +++ b/bindings/python/lyric-py-worker/src/worker.py @@ -11,6 +11,7 @@ import sys import json import asyncio +import traceback # import cloudpickle as pickle # import dill as pickle @@ -43,6 +44,7 @@ def run( success = True except Exception as e: print(f"[Python-InterpreterTask] Exception: {e}") + traceback.print_exc() success = False stdout, stderr = capture.get_output() @@ -91,6 +93,7 @@ def run1( success = True except Exception as e: err_msg = str(e) + traceback.print_exc() stdout, stderr = capture.get_output() stderr = stderr or "" stderr += f"\nException:\n{err_msg}" diff --git a/bindings/python/lyric-py/python/lyric/driver.py b/bindings/python/lyric-py/python/lyric/driver.py index 0883363..5edbacf 100644 --- a/bindings/python/lyric-py/python/lyric/driver.py +++ b/bindings/python/lyric-py/python/lyric/driver.py @@ -5,7 +5,7 @@ from lyric_task import Language, LanguageType -from ._py_lyric import PyConfig, PyLocalEnvironmentConfig, PyLyric +from ._py_lyric import PyConfig, PyLocalEnvironmentConfig, PyLyric, PyTaskResourceConfig from .config import DEFAULT_WORKER_PATH from .py_lyric import Lyric @@ -137,6 +137,7 @@ async def exec( lang: LanguageType = Language.PYTHON, worker_name: Optional[str] = None, decode: bool = True, + resources: Optional[PyTaskResourceConfig] = None, ) -> CodeResult: """Execute code in the specified language @@ -145,13 +146,13 @@ async def exec( lang: Programming language of the code (default: Python) worker_name: Optional name of specific worker to use decode: Whether to decode the execution result (default: True) - + resources: Optional resource configuration for the task Returns: CodeResult containing execution status and output """ try: res = await self._lyric.exec( - code, lang=lang, worker_name=worker_name, decode=decode + code, lang=lang, worker_name=worker_name, decode=decode, resources=resources ) stdout = res.get("stdout", "") stderr = res.get("stderr", "") @@ -171,6 +172,7 @@ async def exec1( decode: bool = True, lang: LanguageType = Language.PYTHON, worker_name: Optional[str] = None, + resources: Optional[PyTaskResourceConfig] = None, ) -> CodeResult: """Execute code with input data and a specific function call @@ -182,6 +184,7 @@ async def exec1( decode: Whether to decode the result (default: True) lang: Programming language of the code (default: Python) worker_name: Optional name of specific worker to use + resources: Optional resource configuration for the task Returns: CodeResult containing execution status, output and additional data @@ -196,6 +199,7 @@ async def exec1( decode=decode, lang=lang, worker_name=worker_name, + resources=resources, ) stdout = res.get("stdout", "") stderr = res.get("stderr", "") diff --git a/bindings/python/lyric-py/python/lyric/py_lyric.py b/bindings/python/lyric-py/python/lyric/py_lyric.py index 99996fe..d3cd32e 100644 --- a/bindings/python/lyric-py/python/lyric/py_lyric.py +++ b/bindings/python/lyric-py/python/lyric/py_lyric.py @@ -17,6 +17,7 @@ PyLocalEnvironmentConfig, PyLyric, PyTaskHandle, + PyTaskResourceConfig, ) logger = logging.getLogger(__name__) @@ -323,6 +324,7 @@ async def exec( worker_name: Optional[str] = None, decode: bool = True, exec_env: Optional[EXEC_ENV] = None, + resources: Optional[PyTaskResourceConfig] = None, ) -> Union[Dict[str, Any], bytes]: """Execute code @@ -332,11 +334,12 @@ async def exec( worker_name: Optional worker name to use decode: Whether to decode the result exec_env: Optional execution environment configuration + resources: Optional task resources """ lang_instance = Language.parse(lang) handle = await self._get_handler(lang_instance, worker_name, exec_env) - script_res = await handle.handle.exec(lang_instance.name, code, decode=decode) + script_res = await handle.handle.exec(lang_instance.name, code, decode=decode, resources=resources) try: encoded = script_res.data @@ -358,6 +361,7 @@ async def exec1( lang: LanguageType = Language.PYTHON, worker_name: Optional[str] = None, exec_env: Optional[EXEC_ENV] = None, + resources: Optional[PyTaskResourceConfig] = None, ) -> Union[Tuple[Dict[str, Any], Dict[str, Any]], Tuple[bytes, bytes]]: """Execute code with input @@ -370,6 +374,7 @@ async def exec1( lang: Language type of the code worker_name: Optional worker name to use exec_env: Optional execution environment configuration + resources: Optional task resources """ lang_instance = Language.parse(lang) handle = await self._get_handler(lang_instance, worker_name, exec_env) @@ -381,6 +386,7 @@ async def exec1( input=input_bytes, encode=encode, decode=decode, + resources=resources, ) res_bytes = bytes(script_res[0].data) diff --git a/bindings/python/lyric-py/src/handle.rs b/bindings/python/lyric-py/src/handle.rs index 91a5fac..2fea665 100644 --- a/bindings/python/lyric-py/src/handle.rs +++ b/bindings/python/lyric-py/src/handle.rs @@ -3,6 +3,7 @@ use futures::TryFutureExt; use std::future::Future; use std::sync::Arc; // use pyo3::{pyclass, pymethods, PyErr, PyObject, PyRef, PyRefMut, PyResult}; +use crate::resource::PyTaskResourceConfig; use crate::task::{PyDataObject, PyTaskStateInfo}; use lyric::task_ext::{ClientType, MsgpackDeserializeExt, MsgpackSerializeExt, TaskHandle}; use lyric::{Lyric, TaskDescription, TokioRuntime}; @@ -41,8 +42,12 @@ pub struct PyTaskHandle { #[pymethods] impl PyTaskHandle { - #[pyo3(name = "run", signature = (args))] - async fn run(&self, args: PyTaskCallArgs) -> PyResult { + #[pyo3(name = "run", signature = (args, resources = None))] + async fn run( + &self, + args: PyTaskCallArgs, + resources: Option, + ) -> PyResult { use lyric_wasm_runtime::capability::wrpc::lyric::task::{binary_task, types}; let req_data = args .data @@ -50,6 +55,7 @@ impl PyTaskHandle { "data is required", ))?; let req = types::BinaryRequest { + resources: resources.map(|r| r.into_rpc()), protocol: 1_u32, data: req_data.data.into(), }; @@ -79,10 +85,17 @@ impl PyTaskHandle { Ok(PyDataObject::from(response_data)) } - #[pyo3(name = "exec", signature = (lang, code, decode = true))] - async fn exec(&self, lang: String, code: String, decode: bool) -> PyResult { + #[pyo3(name = "exec", signature = (lang, code, decode = true, resources = None))] + async fn exec( + &self, + lang: String, + code: String, + decode: bool, + resources: Option, + ) -> PyResult { use lyric_wasm_runtime::capability::wrpc::lyric::task::{interpreter_task, types}; let req = types::InterpreterRequest { + resources: resources.map(|r| r.into_rpc()), protocol: 1_u32, lang, code, @@ -108,7 +121,7 @@ impl PyTaskHandle { .await } - #[pyo3(name = "exec1", signature = (lang, code, call_name, input, encode, decode = true))] + #[pyo3(name = "exec1", signature = (lang, code, call_name, input, encode, decode = true, resources = None))] async fn exec1( &self, lang: String, @@ -117,9 +130,11 @@ impl PyTaskHandle { input: Vec, encode: bool, decode: bool, + resources: Option, ) -> PyResult> { use lyric_wasm_runtime::capability::wrpc::lyric::task::{interpreter_task, types}; let req = types::InterpreterRequest { + resources: resources.map(|r| r.into_rpc()), protocol: 1_u32, lang, code, diff --git a/bindings/python/lyric-py/src/lib.rs b/bindings/python/lyric-py/src/lib.rs index adcad63..da546d4 100644 --- a/bindings/python/lyric-py/src/lib.rs +++ b/bindings/python/lyric-py/src/lib.rs @@ -3,6 +3,7 @@ mod env; mod error; mod handle; mod lyric; +mod resource; mod task; mod types; @@ -11,6 +12,10 @@ use config::{PyConfig, PyDriverConfig, PyWorkerConfig}; use env::{PyDockerEnvironmentConfig, PyEnvironmentConfig, PyLocalEnvironmentConfig}; use lyric::PyLyric; use pyo3::prelude::*; +use resource::{ + PyTaskCpuConfig, PyTaskFilePerms, PyTaskFsConfig, PyTaskInstanceLimits, PyTaskMemoryConfig, + PyTaskNetworkConfig, PyTaskResourceConfig, +}; use std::sync::OnceLock; use task::{ PyDataObject, PyExecutionUnit, PyStreamDataObjectIter, PyTaskInfo, PyTaskOutputObject, @@ -46,6 +51,14 @@ fn _py_lyric(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::().unwrap(); m.add_class::().unwrap(); m.add_class::().unwrap(); + /// Resource Config + m.add_class::().unwrap(); + m.add_class::().unwrap(); + m.add_class::().unwrap(); + m.add_class::().unwrap(); + m.add_class::().unwrap(); + m.add_class::().unwrap(); + m.add_class::().unwrap(); m.add_function(wrap_pyfunction!(from_python_iterator, m)?); Ok(()) } diff --git a/bindings/python/lyric-py/src/lyric.rs b/bindings/python/lyric-py/src/lyric.rs index b8db6bd..792c585 100644 --- a/bindings/python/lyric-py/src/lyric.rs +++ b/bindings/python/lyric-py/src/lyric.rs @@ -230,6 +230,7 @@ impl PyLyric { let handler = state_info.task_handler(component_id).unwrap(); let output = if let Some(task_input) = task_input { let req = types::BinaryRequest { + resources: None, protocol: 1_u32, data: task_input.data.into(), }; @@ -249,6 +250,7 @@ impl PyLyric { } } else { let req = types::InterpreterRequest { + resources: None, protocol: 1_u32, lang: "rust".to_string(), code: "hello".to_string(), diff --git a/bindings/python/lyric-py/src/resource.rs b/bindings/python/lyric-py/src/resource.rs new file mode 100644 index 0000000..87ddd45 --- /dev/null +++ b/bindings/python/lyric-py/src/resource.rs @@ -0,0 +1,203 @@ +use lyric_utils::resource::*; +use lyric_wasm_runtime::capability::rpc_task; +use lyric_wasm_runtime::resource::*; +use pyo3::prelude::*; + +#[pyclass] +#[derive(Clone)] +pub struct PyTaskCpuConfig { + inner: CpuConfig, +} + +#[pyclass] +#[derive(Clone)] +pub struct PyTaskMemoryConfig { + inner: MemoryConfig, +} + +#[pyclass] +#[derive(Clone)] +pub struct PyTaskNetworkConfig { + inner: NetworkConfig, +} + +#[pyclass] +#[derive(Clone)] +pub struct PyTaskFilePerms { + inner: FilePerms, +} + +#[pyclass] +#[derive(Clone)] +pub struct PyTaskFsConfig { + inner: FsConfig, +} + +#[pyclass] +#[derive(Clone)] +pub struct PyTaskInstanceLimits { + inner: InstanceLimits, +} + +#[pyclass] +#[derive(Clone)] +pub struct PyTaskResourceConfig { + inner: ResourceConfig, +} + +#[pymethods] +impl PyTaskCpuConfig { + #[new] + #[pyo3(signature = (cpu_cores=None, cpu_shares=None, cpu_quota=None, cpu_period=None))] + fn new( + cpu_cores: Option, + cpu_shares: Option, + cpu_quota: Option, + cpu_period: Option, + ) -> Self { + PyTaskCpuConfig { + inner: CpuConfig { + cpu_cores, + cpu_shares, + cpu_quota, + cpu_period, + }, + } + } +} + +#[pymethods] +impl PyTaskMemoryConfig { + #[new] + #[pyo3(signature = (memory_limit=None))] + fn new(memory_limit: Option) -> Self { + PyTaskMemoryConfig { + inner: MemoryConfig { memory_limit }, + } + } +} + +#[pymethods] +impl PyTaskNetworkConfig { + #[new] + #[pyo3(signature = (enable_network=None, ingress_limit_kbps=None, egress_limit_kbps=None, allowed_hosts=None, allowed_ports=None))] + fn new( + enable_network: Option, + ingress_limit_kbps: Option, + egress_limit_kbps: Option, + allowed_hosts: Option>, + allowed_ports: Option>, + ) -> Self { + PyTaskNetworkConfig { + inner: NetworkConfig { + enable_network: enable_network.unwrap_or_default(), + ingress_limit_kbps, + egress_limit_kbps, + allowed_hosts, + allowed_ports, + }, + } + } +} + +#[pymethods] +impl PyTaskFilePerms { + #[new] + #[pyo3(signature = (inner=None))] + fn new(inner: Option) -> Self { + PyTaskFilePerms { + inner: FilePerms::from_bits_truncate(inner.unwrap_or_default()), + } + } +} + +#[pymethods] +impl PyTaskFsConfig { + #[new] + #[pyo3(signature = (preopens=None, fs_size_limit=None, temp_dir=None))] + fn new( + preopens: Option>, + fs_size_limit: Option, + temp_dir: Option, + ) -> Self { + let mut r_preopens = Vec::new(); + for (host_path, container_path, dir_perms, file_perms) in preopens.unwrap_or_default() { + let mut r_dir_perms = FilePerms::empty(); + let mut r_file_perms = FilePerms::empty(); + if dir_perms & 0b1 != 0 { + r_dir_perms |= FilePerms::READ; + } + if dir_perms & 0b10 != 0 { + r_dir_perms |= FilePerms::WRITE; + } + + if file_perms & 0b1 != 0 { + r_file_perms |= FilePerms::READ; + } + if file_perms & 0b10 != 0 { + r_file_perms |= FilePerms::WRITE; + } + r_preopens.push((host_path, container_path, r_dir_perms, r_file_perms)); + } + PyTaskFsConfig { + inner: FsConfig { + preopens: r_preopens, + fs_size_limit, + temp_dir, + }, + } + } +} + +#[pymethods] +impl PyTaskInstanceLimits { + #[new] + #[pyo3(signature = (max_instances=None, max_tables=None, max_table_elements=None))] + fn new( + max_instances: Option, + max_tables: Option, + max_table_elements: Option, + ) -> Self { + PyTaskInstanceLimits { + inner: InstanceLimits { + max_instances, + max_tables, + max_table_elements, + }, + } + } +} + +#[pymethods] +impl PyTaskResourceConfig { + #[new] + #[pyo3(signature = (cpu=None, memory=None, network=None, fs=None, instance_limits=None, timeout_ms=None, env_vars=None))] + fn new( + cpu: Option, + memory: Option, + network: Option, + fs: Option, + instance_limits: Option, + timeout_ms: Option, + env_vars: Option>, + ) -> Self { + let env_vars = env_vars.unwrap_or_default(); + PyTaskResourceConfig { + inner: ResourceConfig { + cpu: cpu.map(|c| c.inner), + memory: memory.map(|m| m.inner), + network: network.map(|n| n.inner), + fs: fs.map(|f| f.inner), + instance: instance_limits.map(|i| i.inner), + timeout_ms, + env_vars, + }, + } + } +} + +impl PyTaskResourceConfig { + pub fn into_rpc(self) -> rpc_task::types::ResourceConfig { + self.inner.into() + } +} diff --git a/bindings/python/lyric-task/pyproject.toml b/bindings/python/lyric-task/pyproject.toml index b1eb47c..03b9d11 100644 --- a/bindings/python/lyric-task/pyproject.toml +++ b/bindings/python/lyric-task/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lyric-task" -version = "0.1.3" +version = "0.1.4-rc0" description = "Add your description here" authors = [ { name = "Fangyin Cheng", email = "staneyffer@gmail.com" }, diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml index 8888727..f60b684 100644 --- a/bindings/python/pyproject.toml +++ b/bindings/python/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "python" -version = "0.1.3" +version = "0.1.4-rc0" description = "Add your description here" authors = [ { name = "Fangyin Cheng", email = "staneyffer@gmail.com" } diff --git a/crates/lyric-utils/Cargo.toml b/crates/lyric-utils/Cargo.toml index 7699fb0..6969c5b 100644 --- a/crates/lyric-utils/Cargo.toml +++ b/crates/lyric-utils/Cargo.toml @@ -20,3 +20,4 @@ tracing-subscriber = { workspace = true, features = ["chrono", "env-filter", "j tracing-appender = { workspace = true } lazy_static = { workspace = true } serde_json = "1.0.128" +bitflags = { workspace = true } diff --git a/crates/lyric-utils/src/lib.rs b/crates/lyric-utils/src/lib.rs index 5339436..ae58f0c 100644 --- a/crates/lyric-utils/src/lib.rs +++ b/crates/lyric-utils/src/lib.rs @@ -2,4 +2,5 @@ pub mod err; pub mod log; pub mod net_utils; pub mod prelude; +pub mod resource; pub mod time; diff --git a/crates/lyric-utils/src/resource.rs b/crates/lyric-utils/src/resource.rs new file mode 100644 index 0000000..0cd628c --- /dev/null +++ b/crates/lyric-utils/src/resource.rs @@ -0,0 +1,80 @@ +/// Cpu configuration +/// +#[derive(Debug, Default, Clone)] +pub struct CpuConfig { + /// CPU core limit (e.g. 1.0 represents a full core, 0.5 represents half a core) + pub cpu_cores: Option, + pub cpu_shares: Option, + /// CPU period configuration (microseconds, at most use quota microseconds within period) + pub cpu_quota: Option, + /// CPU period time (microseconds, default 100000, i.e. 100ms) + pub cpu_period: Option, +} + +#[derive(Debug, Default, Clone)] +pub struct MemoryConfig { + /// Memory limit in bytes + pub memory_limit: Option, +} + +#[derive(Debug, Default, Clone)] +pub struct NetworkConfig { + /// Whether to enable network access + pub enable_network: bool, + /// Inbound bandwidth limit (KB/s) + pub ingress_limit_kbps: Option, + /// Outbound bandwidth limit (KB/s) + pub egress_limit_kbps: Option, + /// Allowed host list + pub allowed_hosts: Option>, + /// Allowed port range list (start_port, end_port) + pub allowed_ports: Option>, +} + +bitflags::bitflags! { + #[derive(Copy, Clone, Debug, PartialEq, Eq)] + pub struct FilePerms: usize { + const READ = 0b1; + const WRITE = 0b10; + } +} + +#[derive(Debug, Default, Clone)] +pub struct FsConfig { + /// Pre-mapped directory list (host path, container path, directory permissions, file permissions) + pub preopens: Vec<(String, String, FilePerms, FilePerms)>, + /// File system size limit in bytes + pub fs_size_limit: Option, + /// Temporary directory for wasi + pub temp_dir: Option, +} + +/// Instance limits +#[derive(Debug, Default, Clone)] +pub struct InstanceLimits { + // Max number of instances + pub max_instances: Option, + /// Max number of tables + pub max_tables: Option, + /// Max number of elements in each table + pub max_table_elements: Option, +} + +/// Full resource configuration +#[derive(Debug, Default, Clone)] +pub struct ResourceConfig { + /// CPU configuration + pub cpu: Option, + /// Memory configuration + pub memory: Option, + /// Network configuration + pub network: Option, + /// File system configuration + pub fs: Option, + // Instance limits + pub instance: Option, + /// Task timeout in milliseconds + pub timeout_ms: Option, + /// List of environment variables + pub env_vars: Vec<(String, String)>, +} diff --git a/crates/lyric-wasm-runtime/src/capability.rs b/crates/lyric-wasm-runtime/src/capability.rs index d2bdb45..406ecf2 100644 --- a/crates/lyric-wasm-runtime/src/capability.rs +++ b/crates/lyric-wasm-runtime/src/capability.rs @@ -7,9 +7,6 @@ mod wasmtime_bindings { async: true, tracing: true, trappable_imports: true, - // with: { - // "wasi:io": wasmtime_wasi::bindings::io, - // }, }); } @@ -22,7 +19,7 @@ pub mod wrpc { generate_all, }); } - pub use wasmtime_bindings::lyric::{serialization, task}; pub use wasmtime_bindings::wasi::logging; pub use wasmtime_bindings::Interfaces; +pub use wrpc::lyric::task as rpc_task; diff --git a/crates/lyric-wasm-runtime/src/component/binary.rs b/crates/lyric-wasm-runtime/src/component/binary.rs index 64e70c0..cd49eda 100644 --- a/crates/lyric-wasm-runtime/src/component/binary.rs +++ b/crates/lyric-wasm-runtime/src/component/binary.rs @@ -1,4 +1,5 @@ use super::{Handler, Instance}; +use crate::capability::rpc_task; use crate::new_store; use anyhow::Context; use std::future::Future; @@ -18,8 +19,8 @@ pub mod wrpc_handler_bindings { world: "binary-task", generate_all, with: { - "lyric:task/types@0.2.0": generate, - "lyric:task/binary-task@0.2.0": generate, + "lyric:task/types@0.2.0": crate::capability::rpc_task::types, + "lyric:task/binary-task@0.2.0": generate, } }); } @@ -32,19 +33,21 @@ where async fn run( &self, _: C, - wrpc_handler_bindings::lyric::task::types::BinaryRequest { + rpc_task::types::BinaryRequest { + resources, protocol, - data - }: wrpc_handler_bindings::lyric::task::types::BinaryRequest, - ) -> anyhow::Result> - { + data, + }: rpc_task::types::BinaryRequest, + ) -> anyhow::Result> { let mut store = new_store( &self.engine, self.handler.clone(), self.max_execution_time, None, - ); + resources.map(|r| r.into()), + )?; let request = wasmtime_handler_bindings::lyric::task::types::BinaryRequest { + resources: None, protocol, // Bytes to Vec data: data.into(), @@ -59,13 +62,11 @@ where .await .context("failed to call `lyric:task/interpreter-task`"); res.map(|rs| { - rs.map( - |r| wrpc_handler_bindings::lyric::task::types::BinaryResponse { - protocol: r.protocol, - // Vec to Bytes - data: r.data.into(), - }, - ) + rs.map(|r| rpc_task::types::BinaryResponse { + protocol: r.protocol, + // Vec to Bytes + data: r.data.into(), + }) }) } } diff --git a/crates/lyric-wasm-runtime/src/component/interpreter.rs b/crates/lyric-wasm-runtime/src/component/interpreter.rs index b120cc4..a31a1eb 100644 --- a/crates/lyric-wasm-runtime/src/component/interpreter.rs +++ b/crates/lyric-wasm-runtime/src/component/interpreter.rs @@ -1,4 +1,5 @@ use super::{Handler, Instance}; +use crate::capability::rpc_task; use crate::new_store; use anyhow::Context; use bytes::Bytes; @@ -19,7 +20,7 @@ pub mod wrpc_handler_bindings { world: "interpreter-task", generate_all, with: { - "lyric:task/types@0.2.0": generate, + "lyric:task/types@0.2.0": crate::capability::rpc_task::types, "lyric:task/interpreter-task@0.2.0": generate, } }); @@ -34,21 +35,22 @@ where async fn run( &self, _: C, - wrpc_handler_bindings::lyric::task::types::InterpreterRequest { + rpc_task::types::InterpreterRequest { + resources, protocol, lang, code, - }: wrpc_handler_bindings::lyric::task::types::InterpreterRequest, - ) -> anyhow::Result< - Result, - > { + }: rpc_task::types::InterpreterRequest, + ) -> anyhow::Result> { let mut store = new_store( &self.engine, self.handler.clone(), self.max_execution_time, None, - ); + resources.map(|r| r.into()), + )?; let script = wasmtime_handler_bindings::lyric::task::types::InterpreterRequest { + resources: None, protocol, lang, code, @@ -63,36 +65,35 @@ where .await .context("failed to call `lyric:task/interpreter-task`"); res.map(|rs| { - rs.map( - |r| wrpc_handler_bindings::lyric::task::types::InterpreterResponse { - protocol: r.protocol, - // Vec to Bytes - data: r.data.into(), - }, - ) + rs.map(|r| rpc_task::types::InterpreterResponse { + protocol: r.protocol, + // Vec to Bytes + data: r.data.into(), + }) }) } async fn run1( &self, _: C, - wrpc_handler_bindings::lyric::task::types::InterpreterRequest { + rpc_task::types::InterpreterRequest { + resources, protocol, lang, code, - }: wrpc_handler_bindings::lyric::task::types::InterpreterRequest, + }: rpc_task::types::InterpreterRequest, call_name: String, input: Bytes, - ) -> anyhow::Result< - Result, - > { + ) -> anyhow::Result> { let mut store = new_store( &self.engine, self.handler.clone(), self.max_execution_time, None, - ); + resources.map(|r| r.into()), + )?; let script = wasmtime_handler_bindings::lyric::task::types::InterpreterRequest { + resources: None, protocol, lang, code, @@ -108,14 +109,12 @@ where .await .context("failed to call `lyric:task/interpreter-task`"); res.map(|rs| { - rs.map( - |r| wrpc_handler_bindings::lyric::task::types::InterpreterOutputResponse { - protocol: r.protocol, - // Vec to Bytes - data: r.data.into(), - output: r.output.into(), - }, - ) + rs.map(|r| rpc_task::types::InterpreterOutputResponse { + protocol: r.protocol, + // Vec to Bytes + data: r.data.into(), + output: r.output.into(), + }) }) } } diff --git a/crates/lyric-wasm-runtime/src/component/mod.rs b/crates/lyric-wasm-runtime/src/component/mod.rs index 5854cc2..91e2a2b 100644 --- a/crates/lyric-wasm-runtime/src/component/mod.rs +++ b/crates/lyric-wasm-runtime/src/component/mod.rs @@ -8,6 +8,7 @@ use crate::capability::wrpc::lyric::task; pub use crate::component::logging::Logging; use crate::error::WasmError; use anyhow::{anyhow, Context as _}; +use lyric_utils::resource::ResourceConfig; use std::fmt::Debug; use std::future::Future; use std::pin::Pin; @@ -161,6 +162,8 @@ where table: ResourceTable, shared_resources: SharedResourceTable, timeout: Duration, + resource: Option, + limits: wasmtime::StoreLimits, } impl WasiView for Ctx { @@ -320,7 +323,10 @@ where tracing::debug!(?name, "serving root function"); let func = srv .serve_function( - move || new_store(&engine, handler.clone(), max_execution_time, None), + move || { + new_store(&engine, handler.clone(), max_execution_time, None, None) + .expect("failed to create store") + }, pre, ty, "", @@ -380,7 +386,9 @@ where handler.clone(), max_execution_time, None, + None, ) + .expect("failed to create store") }, pre, ty, @@ -467,7 +475,8 @@ where handler, Duration::from_secs(10), Some("command.wasm"), - ); + None, + )?; let cmd = wasmtime_wasi::bindings::CommandPre::new(self.instance_pre.clone())? .instantiate_async(&mut store) .await @@ -521,14 +530,63 @@ pub fn new_store( handler: H, max_execution_time: Duration, arg0: Option<&str>, -) -> wasmtime::Store> { + resource: Option, +) -> anyhow::Result>> { + tracing::info!("Creating new store with resource: {:?}", resource); + let resource = resource.unwrap_or_default(); + let table = ResourceTable::new(); let arg0 = arg0.unwrap_or("main.wasm"); - let wasi = WasiCtxBuilder::new() - .args(&[arg0]) // TODO: Configure argv[0] - .inherit_stdio() - .inherit_stderr() - .build(); + + let mut wasi_builder = WasiCtxBuilder::new(); + wasi_builder.args(&[arg0]).inherit_stdio().inherit_stderr(); + + // Configure environment variables + for (key, value) in &resource.env_vars { + wasi_builder.env(key, value); + } + + // Configure preopened directories + if let Some(fs_config) = &resource.fs { + // Configured pre-mapped directory list (host path, container path, directory permissions, file permissions) + for (host_path, guest_path, dir_perms, file_perms) in &fs_config.preopens { + let dir_perms = DirPerms::from_bits_truncate(dir_perms.bits()); + let file_perms = FilePerms::from_bits_truncate(file_perms.bits()); + tracing::info!( + "Pre-mapped directory: host_path={}, guest_path={}, dir_perms={:?}, file_perms={:?}", + host_path, + guest_path, + dir_perms, + file_perms + ); + wasi_builder.preopened_dir(host_path, guest_path, dir_perms, file_perms)?; + } + } + let wasi = wasi_builder.build(); + + let timeout = resource + .timeout_ms + .map(|ms| Duration::from_millis(ms as u64)) + .unwrap_or(max_execution_time); + + let mut limits_builder = wasmtime::StoreLimitsBuilder::new(); + if let Some(memory) = &resource.memory { + if let Some(memory_size) = memory.memory_limit { + limits_builder = limits_builder.memory_size(memory_size as usize); + } + } + if let Some(instance) = &resource.instance { + if let Some(max_instances) = instance.max_instances { + limits_builder = limits_builder.instances(max_instances as usize); + } + if let Some(max_tables) = instance.max_tables { + limits_builder = limits_builder.tables(max_tables as usize); + } + if let Some(max_table_elements) = instance.max_table_elements { + limits_builder = limits_builder.table_elements(max_table_elements); + } + } + let limits = limits_builder.build(); let mut store = wasmtime::Store::new( engine, @@ -538,13 +596,16 @@ pub fn new_store( http: WasiHttpCtx::new(), table, shared_resources: SharedResourceTable::default(), - timeout: max_execution_time, + timeout, + resource: Some(resource), + limits, }, ); /// TODO: Limit the cpu time by setting fuel - /// store.set_fuel() - store.set_epoch_deadline(max_execution_time.as_secs()); - store + // store.set_fuel() + store.limiter(|state| &mut state.limits); + store.set_epoch_deadline(timeout.as_secs()); + Ok(store) } /// This represents a [Stream] of incoming invocations. diff --git a/crates/lyric-wasm-runtime/src/lib.rs b/crates/lyric-wasm-runtime/src/lib.rs index f27cbbe..0250e32 100644 --- a/crates/lyric-wasm-runtime/src/lib.rs +++ b/crates/lyric-wasm-runtime/src/lib.rs @@ -2,6 +2,7 @@ pub mod capability; mod component; pub mod error; mod host; +pub mod resource; mod tcp; mod utils; diff --git a/crates/lyric-wasm-runtime/src/resource.rs b/crates/lyric-wasm-runtime/src/resource.rs new file mode 100644 index 0000000..0d89628 --- /dev/null +++ b/crates/lyric-wasm-runtime/src/resource.rs @@ -0,0 +1,286 @@ +use crate::capability::task::types as wasmtime; +use crate::capability::wrpc::lyric::task::types as wrpc; +use lyric_utils::resource as utils; + +// Define macro to handle conversions between utils <-> wasmtime and utils <-> wrpc +macro_rules! implement_conversions { + ($type_name:ident, {$($field:ident),*}) => { + // utils -> wasmtime + impl From for wasmtime::$type_name { + fn from(value: utils::$type_name) -> Self { + Self { + $($field: value.$field),* + } + } + } + + // wasmtime -> utils + impl From for utils::$type_name { + fn from(value: wasmtime::$type_name) -> Self { + Self { + $($field: value.$field),* + } + } + } + + // utils -> wrpc + impl From for wrpc::$type_name { + fn from(value: utils::$type_name) -> Self { + Self { + $($field: value.$field),* + } + } + } + + // wrpc -> utils + impl From for utils::$type_name { + fn from(value: wrpc::$type_name) -> Self { + Self { + $($field: value.$field),* + } + } + } + }; +} + +// Implement conversions for all configuration types +implement_conversions!(CpuConfig, { + cpu_cores, + cpu_shares, + cpu_quota, + cpu_period +}); + +implement_conversions!(MemoryConfig, { memory_limit }); + +implement_conversions!(NetworkConfig, { + enable_network, + ingress_limit_kbps, + egress_limit_kbps, + allowed_hosts, + allowed_ports +}); + +// Define conversions for FilePerms +macro_rules! implement_file_perms_conversions { + () => { + // utils <-> wasmtime + impl From for wasmtime::FilePerms { + fn from(perms: utils::FilePerms) -> Self { + let mut result = wasmtime::FilePerms::empty(); + if perms.contains(utils::FilePerms::READ) { + result |= wasmtime::FilePerms::READ; + } + if perms.contains(utils::FilePerms::WRITE) { + result |= wasmtime::FilePerms::WRITE; + } + result + } + } + + impl From for utils::FilePerms { + fn from(perms: wasmtime::FilePerms) -> Self { + let mut result = utils::FilePerms::empty(); + if perms.contains(wasmtime::FilePerms::READ) { + result |= utils::FilePerms::READ; + } + if perms.contains(wasmtime::FilePerms::WRITE) { + result |= utils::FilePerms::WRITE; + } + result + } + } + + // utils <-> wrpc + impl From for wrpc::FilePerms { + fn from(perms: utils::FilePerms) -> Self { + let mut result = wrpc::FilePerms::empty(); + if perms.contains(utils::FilePerms::READ) { + result |= wrpc::FilePerms::READ; + } + if perms.contains(utils::FilePerms::WRITE) { + result |= wrpc::FilePerms::WRITE; + } + result + } + } + + impl From for utils::FilePerms { + fn from(perms: wrpc::FilePerms) -> Self { + let mut result = utils::FilePerms::empty(); + if perms.contains(wrpc::FilePerms::READ) { + result |= utils::FilePerms::READ; + } + if perms.contains(wrpc::FilePerms::WRITE) { + result |= utils::FilePerms::WRITE; + } + result + } + } + }; +} + +/// Complex type conversions for FsConfig +macro_rules! implement_fs_config_conversions { + () => { + // utils <-> wasmtime + impl From for wasmtime::FsConfig { + fn from(config: utils::FsConfig) -> Self { + wasmtime::FsConfig { + preopens: config + .preopens + .into_iter() + .map(|(host_path, container_path, dir_perms, file_perms)| { + ( + host_path, + container_path, + dir_perms.into(), + file_perms.into(), + ) + }) + .collect(), + fs_size_limit: config.fs_size_limit, + temp_dir: config.temp_dir, + } + } + } + + impl From for utils::FsConfig { + fn from(config: wasmtime::FsConfig) -> Self { + utils::FsConfig { + preopens: config + .preopens + .into_iter() + .map(|(host_path, container_path, dir_perms, file_perms)| { + ( + host_path, + container_path, + dir_perms.into(), + file_perms.into(), + ) + }) + .collect(), + fs_size_limit: config.fs_size_limit, + temp_dir: config.temp_dir, + } + } + } + + // utils <-> wrpc + impl From for wrpc::FsConfig { + fn from(config: utils::FsConfig) -> Self { + wrpc::FsConfig { + preopens: config + .preopens + .into_iter() + .map(|(host_path, container_path, dir_perms, file_perms)| { + ( + host_path, + container_path, + dir_perms.into(), + file_perms.into(), + ) + }) + .collect(), + fs_size_limit: config.fs_size_limit, + temp_dir: config.temp_dir, + } + } + } + + impl From for utils::FsConfig { + fn from(config: wrpc::FsConfig) -> Self { + utils::FsConfig { + preopens: config + .preopens + .into_iter() + .map(|(host_path, container_path, dir_perms, file_perms)| { + ( + host_path, + container_path, + dir_perms.into(), + file_perms.into(), + ) + }) + .collect(), + fs_size_limit: config.fs_size_limit, + temp_dir: config.temp_dir, + } + } + } + }; +} + +implement_conversions!(InstanceLimits, { + max_instances, + max_tables, + max_table_elements +}); + +// Implement conversions for ResourceConfig +macro_rules! implement_resource_config_conversions { + () => { + // utils <-> wasmtime + impl From for wasmtime::ResourceConfig { + fn from(config: utils::ResourceConfig) -> Self { + wasmtime::ResourceConfig { + cpu: config.cpu.map(|c| c.into()), + memory: config.memory.map(|m| m.into()), + network: config.network.map(|n| n.into()), + fs: config.fs.map(|f| f.into()), + instance: config.instance.map(|i| i.into()), + timeout_ms: config.timeout_ms, + env_vars: config.env_vars, + } + } + } + + impl From for utils::ResourceConfig { + fn from(config: wasmtime::ResourceConfig) -> Self { + utils::ResourceConfig { + cpu: config.cpu.map(|c| c.into()), + memory: config.memory.map(|m| m.into()), + network: config.network.map(|n| n.into()), + fs: config.fs.map(|f| f.into()), + instance: config.instance.map(|i| i.into()), + timeout_ms: config.timeout_ms, + env_vars: config.env_vars, + } + } + } + + // utils <-> wrpc + impl From for wrpc::ResourceConfig { + fn from(config: utils::ResourceConfig) -> Self { + wrpc::ResourceConfig { + cpu: config.cpu.map(|c| c.into()), + memory: config.memory.map(|m| m.into()), + network: config.network.map(|n| n.into()), + fs: config.fs.map(|f| f.into()), + instance: config.instance.map(|i| i.into()), + timeout_ms: config.timeout_ms, + env_vars: config.env_vars, + } + } + } + + impl From for utils::ResourceConfig { + fn from(config: wrpc::ResourceConfig) -> Self { + utils::ResourceConfig { + cpu: config.cpu.map(|c| c.into()), + memory: config.memory.map(|m| m.into()), + network: config.network.map(|n| n.into()), + fs: config.fs.map(|f| f.into()), + instance: config.instance.map(|i| i.into()), + timeout_ms: config.timeout_ms, + env_vars: config.env_vars, + } + } + } + }; +} + +// Implement conversions for all types +implement_file_perms_conversions!(); +implement_fs_config_conversions!(); +implement_resource_config_conversions!(); diff --git a/crates/lyric-wasm-runtime/wit/deps/task/types.wit b/crates/lyric-wasm-runtime/wit/deps/task/types.wit index 1061a37..0f1ab9a 100644 --- a/crates/lyric-wasm-runtime/wit/deps/task/types.wit +++ b/crates/lyric-wasm-runtime/wit/deps/task/types.wit @@ -1,11 +1,92 @@ interface types { + + /// CPU Resource Configuration + record cpu-config { + /// CPU core limit (e.g. 1.0 represents a full core, 0.5 represents half a core) + cpu-cores: option, + /// CPU share value (similar to --cpu-shares in docker, default 1024) + cpu-shares: option, + /// CPU period configuration (microseconds, at most use quota microseconds within period) + cpu-quota: option, + /// CPU period time (microseconds, default 100000, i.e. 100ms) + cpu-period: option + } + + /// Memory Resource Configuration + record memory-config { + /// Memory limit in bytes + memory-limit: option + } + + /// Network Resource Configuration + record network-config { + /// Whether to enable network access + enable-network: bool, + /// Inbound bandwidth limit (KB/s) + ingress-limit-kbps: option, + /// Outbound bandwidth limit (KB/s) + egress-limit-kbps: option, + /// Allowed host list + allowed-hosts: option>, + /// Allowed port range list (start_port, end_port) + allowed-ports: option>> + } + + /// File system permission + flags file-perms { + /// Read permission + read, + /// Write permission + write, + } + + /// File system configuration + record fs-config { + /// Pre-mapped directory list (host path, container path, directory permissions, file permissions) + preopens: list>, + /// File system size limit in bytes + fs-size-limit: option, + /// Temporary directory for wasi + temp-dir: option + } + + /// Instance limits + record instance-limits { + // Max number of instances + max-instances: option, + /// Max number of tables + max-tables: option, + /// Max number of elements in each table + max-table-elements: option + } + + /// Full resource configuration + record resource-config { + /// CPU configuration + cpu: option, + /// Memory configuration + memory: option, + /// Network configuration + network: option, + /// File system configuration + fs: option, + // Instance limits + instance: option, + /// Task timeout in milliseconds + timeout-ms: option, + /// List of environment variables + env-vars: list> + } + /// A request to interpret a script record interpreter-request { + /// Resource configuration + resources: option, /// The protocol version of the interpreter protocol: u32, /// The language of the script lang: string, - /// The script to be interpreted + /// The script to be interpreted code: string } @@ -29,6 +110,8 @@ interface types { } record binary-request { + /// Resource configuration + resources: option, protocol: u32, data: list } diff --git a/crates/lyric/src/lyric.rs b/crates/lyric/src/lyric.rs index 29affad..db5381f 100644 --- a/crates/lyric/src/lyric.rs +++ b/crates/lyric/src/lyric.rs @@ -12,8 +12,11 @@ use lyric_wasm_runtime::WasmRuntime; use std::collections::HashMap; use std::fmt::Debug; use std::future::Future; +use std::net::SocketAddr; use std::pin::Pin; use std::sync::Arc; +use std::time::Duration; +use tokio::net::TcpListener; use tokio::sync::{mpsc, oneshot, Mutex}; use tracing; use uuid::Uuid; @@ -122,11 +125,20 @@ impl Lyric { let tx_to_core = self.inner.tx_to_core.clone(); + tracing::info!("Starting driver server on {}", addr); + let socket_addr = addr + .parse() + .map_err(|e| Error::InternalError(format!("Failed to parse address: {}", e)))?; + + self.inner.runtime.runtime.block_on(async { + check_address_availability(&addr, Duration::from_secs(5), 3, Duration::from_secs(1)) + .await + .map_err(|e| Error::InternalError(format!("Address check failed: {}", e))) + })?; + tracing::info!("Server listening on {}", addr); + self.inner.runtime.runtime.spawn(async move { - tracing::info!("Starting driver server on {}", addr); let driver_service = DriverService::new(tx_to_core); - let socket_addr = addr.parse().unwrap(); - tracing::info!("Server listening on {}", addr); let _ = Server::builder() .add_service(DriverRpcServer::new(driver_service)) .serve_with_shutdown(socket_addr, async { @@ -157,12 +169,21 @@ impl Lyric { let inner = self.inner.clone(); + let socket_addr = addr + .parse() + .map_err(|e| Error::InternalError(format!("Failed to parse address: {}", e)))?; + + self.inner.runtime.runtime.block_on(async { + check_address_availability(&addr, Duration::from_secs(5), 3, Duration::from_secs(1)) + .await + .map_err(|e| Error::InternalError(format!("Address check failed: {}", e))) + })?; + + tracing::info!("LyricServer {} listening on {}", worker_id, addr); + tracing::info!("Connect to driver: {}", driver_addr); + self.inner.runtime.runtime.spawn(async move { - tracing::info!("Starting worker server on {}", addr); let worker_service = WorkerService::new(tx_to_core, pg); - let socket_addr = addr.parse().unwrap(); - tracing::info!("LyricServer {} listening on {}", worker_id, addr); - tracing::info!("Connect to driver: {}", driver_addr); let (tx_server, rx_server) = oneshot::channel(); // Start wasm runtime @@ -379,3 +400,53 @@ impl Lyric { res } } + +pub(crate) async fn check_address_availability( + addr: &str, + timeout: Duration, + retry_times: usize, + retry_interval: Duration, +) -> Result<(), Error> { + let socket_addr: SocketAddr = addr + .parse() + .map_err(|e| Error::InternalError(format!("Failed to parse address {}: {}", addr, e)))?; + let mut current_try = 0; + loop { + tracing::info!( + "Checking address availability: {} (attempt {}/{})", + addr, + current_try + 1, + retry_times + ); + + match tokio::time::timeout(timeout, TcpListener::bind(&socket_addr)).await { + Ok(result) => match result { + Ok(_) => { + tracing::info!("Address {} is available", addr); + return Ok(()); + } + Err(e) => { + tracing::warn!("Failed to bind to {}: {}", addr, e); + } + }, + Err(_) => { + tracing::warn!("Timeout while checking address: {}", addr); + return Err(Error::InternalError(format!( + "Timeout while checking address: {}", + addr + ))); + } + } + + current_try += 1; + if current_try >= retry_times { + return Err(Error::InternalError(format!( + "Failed to bind to address: {}", + addr + ))); + } + + tracing::info!("Retrying in {} seconds...", retry_interval.as_secs()); + tokio::time::sleep(retry_interval).await; + } +}