diff --git a/Cargo.lock b/Cargo.lock index f558839f..693d1d8f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,6 +56,7 @@ dependencies = [ "ctrlc 3.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "fs_extra 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-preview 0.3.0-alpha.17 (registry+https://github.com/rust-lang/crates.io-index)", "http 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", @@ -66,6 +67,7 @@ dependencies = [ "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)", "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "tokio 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-threadpool 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)", "tower-grpc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "tower-hyper 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "tower-service 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -424,6 +426,20 @@ name = "futures" version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "futures-channel-preview" +version = "0.3.0-alpha.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures-core-preview 0.3.0-alpha.17 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-sink-preview 0.3.0-alpha.17 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "futures-core-preview" +version = "0.3.0-alpha.17" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "futures-cpupool" version = "0.1.8" @@ -433,6 +449,58 @@ dependencies = [ "num_cpus 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "futures-executor-preview" +version = "0.3.0-alpha.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures-channel-preview 0.3.0-alpha.17 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-core-preview 0.3.0-alpha.17 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-util-preview 0.3.0-alpha.17 (registry+https://github.com/rust-lang/crates.io-index)", + "num_cpus 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "futures-io-preview" +version = "0.3.0-alpha.17" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "futures-preview" +version = "0.3.0-alpha.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures-channel-preview 0.3.0-alpha.17 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-core-preview 0.3.0-alpha.17 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-executor-preview 0.3.0-alpha.17 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-io-preview 0.3.0-alpha.17 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-sink-preview 0.3.0-alpha.17 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-util-preview 0.3.0-alpha.17 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "futures-sink-preview" +version = "0.3.0-alpha.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures-core-preview 0.3.0-alpha.17 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "futures-util-preview" +version = "0.3.0-alpha.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-channel-preview 0.3.0-alpha.17 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-core-preview 0.3.0-alpha.17 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-io-preview 0.3.0-alpha.17 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-sink-preview 0.3.0-alpha.17 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "pin-utils 0.1.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)", + "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "generic-array" version = "0.12.0" @@ -544,6 +612,7 @@ name = "http-example" version = "0.1.0" dependencies = [ "azure-functions 0.9.0", + "futures-preview 0.3.0-alpha.17 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.94 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)", @@ -574,11 +643,11 @@ dependencies = [ "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", "tokio 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-buf 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-executor 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-executor 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-reactor 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-tcp 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-threadpool 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-threadpool 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-timer 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", "want 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -863,6 +932,11 @@ dependencies = [ "fixedbitset 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "pin-utils" +version = "0.1.0-alpha.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "ppv-lite86" version = "0.2.5" @@ -1383,13 +1457,13 @@ dependencies = [ "num_cpus 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-codec 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-current-thread 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-executor 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-executor 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-fs 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-reactor 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-sync 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-tcp 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-threadpool 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-threadpool 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-timer 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-udp 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-uds 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1421,12 +1495,12 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-executor 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-executor 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "tokio-executor" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "crossbeam-utils 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1440,7 +1514,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-threadpool 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-threadpool 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1466,7 +1540,7 @@ dependencies = [ "num_cpus 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-executor 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-executor 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-sync 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1495,7 +1569,7 @@ dependencies = [ [[package]] name = "tokio-threadpool" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "crossbeam-deque 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1506,7 +1580,7 @@ dependencies = [ "num_cpus 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-executor 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-executor 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1517,7 +1591,7 @@ dependencies = [ "crossbeam-utils 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)", "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-executor 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-executor 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1605,7 +1679,7 @@ dependencies = [ "hyper 0.12.29 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-buf 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-executor 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-executor 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", "tower-http-util 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "tower-service 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1828,7 +1902,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" "checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" "checksum futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)" = "45dc39533a6cae6da2b56da48edae506bb767ec07370f86f70fc062e9d435869" +"checksum futures-channel-preview 0.3.0-alpha.17 (registry+https://github.com/rust-lang/crates.io-index)" = "21c71ed547606de08e9ae744bb3c6d80f5627527ef31ecf2a7210d0e67bc8fae" +"checksum futures-core-preview 0.3.0-alpha.17 (registry+https://github.com/rust-lang/crates.io-index)" = "4b141ccf9b7601ef987f36f1c0d9522f76df3bba1cf2e63bfacccc044c4558f5" "checksum futures-cpupool 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "ab90cde24b3319636588d0c35fe03b1333857621051837ed769faefb4c2162e4" +"checksum futures-executor-preview 0.3.0-alpha.17 (registry+https://github.com/rust-lang/crates.io-index)" = "87ba260fe51080ba37f063ad5b0732c4ff1f737ea18dcb67833d282cdc2c6f14" +"checksum futures-io-preview 0.3.0-alpha.17 (registry+https://github.com/rust-lang/crates.io-index)" = "082e402605fcb8b1ae1e5ba7d7fdfd3e31ef510e2a8367dd92927bb41ae41b3a" +"checksum futures-preview 0.3.0-alpha.17 (registry+https://github.com/rust-lang/crates.io-index)" = "bf25f91c8a9a1f64c451e91b43ba269ed359b9f52d35ed4b3ce3f9c842435867" +"checksum futures-sink-preview 0.3.0-alpha.17 (registry+https://github.com/rust-lang/crates.io-index)" = "4309a25a1069a1f3c10647b227b9afe6722b67a030d3f00a9cbdc171fc038de4" +"checksum futures-util-preview 0.3.0-alpha.17 (registry+https://github.com/rust-lang/crates.io-index)" = "af8198c48b222f02326940ce2b3aa9e6e91a32886eeaad7ca3b8e4c70daa3f4e" "checksum generic-array 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3c0f28c2f5bfb5960175af447a2da7c18900693738343dc896ffbcabd9839592" "checksum getrandom 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "e65cce4e5084b14874c4e7097f38cab54f47ee554f9194673456ea379dcc4c55" "checksum h2 0.1.23 (registry+https://github.com/rust-lang/crates.io-index)" = "1e42e3daed5a7e17b12a0c23b5b2fbff23a925a570938ebee4baca1a9a1a2240" @@ -1875,6 +1956,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum pest_generator 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "63120576c4efd69615b5537d3d052257328a4ca82876771d6944424ccfd9f646" "checksum pest_meta 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f249ea6de7c7b7aba92b4ff4376a994c6dbd98fd2166c89d5c4947397ecb574d" "checksum petgraph 0.4.13 (registry+https://github.com/rust-lang/crates.io-index)" = "9c3659d1ee90221741f65dd128d9998311b0e40c5d3c23a62445938214abce4f" +"checksum pin-utils 0.1.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5894c618ce612a3fa23881b152b608bafb8c56cfc22f434a3ba3120b40f7b587" "checksum ppv-lite86 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e3cbf9f658cdb5000fcf6f362b8ea2ba154b9f146a61c7a20d647034c6b6561b" "checksum proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)" = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" "checksum prost 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "96d14b1c185652833d24aaad41c5832b0be5616a590227c1fbff57c616754b23" @@ -1933,13 +2015,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum tokio-buf 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8fb220f46c53859a4b7ec083e41dec9778ff0b1851c0942b211edb89e0ccdc46" "checksum tokio-codec 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5c501eceaf96f0e1793cf26beb63da3d11c738c4a943fdf3746d81d64684c39f" "checksum tokio-current-thread 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "d16217cad7f1b840c5a97dfb3c43b0c871fef423a6e8d2118c604e843662a443" -"checksum tokio-executor 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "83ea44c6c0773cc034771693711c35c677b4b5a4b21b9e7071704c54de7d555e" +"checksum tokio-executor 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "0f27ee0e6db01c5f0b2973824547ce7e637b2ed79b891a9677b0de9bd532b6ac" "checksum tokio-fs 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "3fe6dc22b08d6993916647d108a1a7d15b9cd29c4f4496c62b92c45b5041b7af" "checksum tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "5090db468dad16e1a7a54c8c67280c5e4b544f3d3e018f0b913b400261f85926" "checksum tokio-reactor 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "6af16bfac7e112bea8b0442542161bfc41cbfa4466b580bdda7d18cb88b911ce" "checksum tokio-sync 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2162248ff317e2bc713b261f242b69dbb838b85248ed20bb21df56d60ea4cae7" "checksum tokio-tcp 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1d14b10654be682ac43efee27401d792507e30fd8d26389e1da3b185de2e4119" -"checksum tokio-threadpool 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "72558af20be886ea124595ea0f806dd5703b8958e4705429dd58b3d8231f72f2" +"checksum tokio-threadpool 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "90ca01319dea1e376a001e8dc192d42ebde6dd532532a5bad988ac37db365b19" "checksum tokio-timer 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "f2106812d500ed25a4f38235b9cae8f78a09edf43203e16e59c3b769a342a60e" "checksum tokio-udp 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "66268575b80f4a4a710ef83d087fdfeeabdce9b74c797535fbac18a2cb906e92" "checksum tokio-uds 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "037ffc3ba0e12a0ab4aca92e5234e0dedeb48fddf6ccd260f1f150a36a9f2445" diff --git a/README.md b/README.md index 3b4d8f66..398a4029 100644 --- a/README.md +++ b/README.md @@ -21,21 +21,44 @@ in [Rust](https://www.rust-lang.org/). A simple HTTP-triggered Azure Function: ```rust -use azure_functions::bindings::{HttpRequest, HttpResponse}; -use azure_functions::func; +use azure_functions::{ + bindings::{HttpRequest, HttpResponse}, + func, +}; #[func] pub fn greet(req: HttpRequest) -> HttpResponse { - // Log the message with the Azure Functions Host - info!("Request: {:?}", req); - format!( "Hello from Rust, {}!\n", req.query_params().get("name").map_or("stranger", |x| x) - ).into() + ) + .into() +} +``` + +Azure Functions for Rust supports [async](https://rust-lang.github.io/rfcs/2394-async_await.html) functions when compiled with a nightly compiler and with the `unstable` feature enabled: + +```rust +use azure_functions::{ + bindings::{HttpRequest, HttpResponse}, + func, +}; +use futures::future::ready; + +#[func] +pub async fn greet_async(req: HttpRequest) -> HttpResponse { + // Use ready().await to simply demonstrate the async/await feature + ready(format!( + "Hello from Rust, {}!\n", + req.query_params().get("name").map_or("stranger", |x| x) + )) + .await + .into() } ``` +See [Building an async Azure Functions application](#building-an-async-azure-functions-application) for more information. + ## Get Started - [More Examples](https://github.com/peterhuene/azure-functions-rs/tree/master/examples) @@ -54,6 +77,7 @@ pub fn greet(req: HttpRequest) -> HttpResponse { - [Creating a new Azure Functions application](#creating-a-new-azure-functions-application) - [Adding a simple HTTP-triggered application](#adding-a-simple-http-triggered-application) - [Building the Azure Functions application](#building-the-azure-functions-application) +- [Building an async Azure Functions application](#building-an-async-azure-functions-application) - [Running the Azure Functions application](#running-the-azure-functions-application) - [Debugging the Azure Functions application](#debugging-the-azure-functions-application) - [Deploying the Azure Functions application](#deploying-the-azure-functions-application) @@ -138,6 +162,24 @@ cargo build --features unstable This enables Azure Functions for Rust to emit diagnostic messages that will include the position of an error within an attribute. +## Building an async Azure Functions application + +To build with support for async Azure Functions, add the following to your `Cargo.toml`: + +```toml +[dependencies] +futures-preview = { version = "0.3.0-alpha.17", optional = true } + +[features] +unstable = ["azure-functions/unstable", "futures-preview"] +``` + +And then build with the `unstable` feature: + +```bash +cargo build --features unstable +``` + ## Running the Azure Functions application To build and run your Azure Functions application, use `cargo func run`: @@ -146,6 +188,12 @@ To build and run your Azure Functions application, use `cargo func run`: cargo func run ``` +If you need to enable the `unstable` feature, pass the `--features` option to cargo: + +```bash +cargo func run -- --features unstable +``` + The `cargo func run` command builds and runs your application locally using the Azure Function Host that was installed by the Azure Functions Core Tools. @@ -228,9 +276,6 @@ The current list of supported bindings: | [Table](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.Table.html) | Input and Ouput Table | in, out | No | | [TimerInfo](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.TimerInfo.html) | Timer Trigger | in | No | | [TwilioSmsMessage](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.TwilioSmsMessage.html) | Twilio SMS Message Output | out | Yes | Yes | -| [Context](https://docs.rs/azure-functions/latest/azure_functions/struct.Context.html)* | Invocation Context | N/A | N/A | - -\****Note: the `Context` binding is not an Azure Functions binding; it is used to pass information about the function being invoked.*** More bindings will be implemented in the future, including support for retreiving data from custom bindings. @@ -358,4 +403,3 @@ pub fn example(...) -> ((), Blob) { ``` For the above example, there is no `$return` binding and the Azure Function "returns" no value. Instead, a single output binding named `output1` is used. - diff --git a/azure-functions-codegen/src/func.rs b/azure-functions-codegen/src/func.rs index 73b9e71a..4b527499 100644 --- a/azure-functions-codegen/src/func.rs +++ b/azure-functions-codegen/src/func.rs @@ -7,7 +7,7 @@ use azure_functions_shared::codegen::{ Binding, BindingFactory, INPUT_BINDINGS, INPUT_OUTPUT_BINDINGS, OUTPUT_BINDINGS, TRIGGERS, VEC_INPUT_BINDINGS, VEC_OUTPUT_BINDINGS, }, - get_string_value, iter_attribute_args, last_segment_in_path, macro_panic, Function, + get_string_value, iter_attribute_args, last_segment_in_path, macro_panic, Function, InvokerFn, }; use invoker::Invoker; use output_bindings::OutputBindings; @@ -23,7 +23,6 @@ use syn::{ pub const OUTPUT_BINDING_PREFIX: &str = "output"; const RETURN_BINDING_NAME: &str = "$return"; -const CONTEXT_TYPE_NAME: &str = "Context"; fn validate_function(func: &ItemFn) { match func.vis { @@ -224,19 +223,6 @@ fn bind_input_type( has_trigger: bool, binding_args: &mut HashMap, ) -> Binding { - let last_segment = last_segment_in_path(&tp.path); - let type_name = last_segment.ident.to_string(); - - if type_name == CONTEXT_TYPE_NAME { - if let Some(m) = mutability { - macro_panic( - m.span(), - "context bindings cannot be passed by mutable reference", - ); - } - return Binding::Context; - } - let factory = get_input_binding_factory(tp, mutability, has_trigger); match pattern { @@ -498,7 +484,27 @@ pub fn func_impl( func.name = Cow::Owned(target_name.clone()); } - func.invoker_name = Some(Cow::Owned(invoker.name())); + match target.asyncness { + Some(asyncness) => { + if cfg!(feature = "unstable") { + func.invoker = Some(azure_functions_shared::codegen::Invoker { + name: Cow::Owned(invoker.name()), + invoker_fn: InvokerFn::Async(None), + }); + } else { + macro_panic( + asyncness.span(), + "async Azure Functions require a nightly compiler with the 'unstable' feature enabled", + ); + } + } + None => { + func.invoker = Some(azure_functions_shared::codegen::Invoker { + name: Cow::Owned(invoker.name()), + invoker_fn: InvokerFn::Sync(None), + }); + } + } let const_name = Ident::new( &format!("__{}_FUNCTION", target_name.to_uppercase()), diff --git a/azure-functions-codegen/src/func/invoker.rs b/azure-functions-codegen/src/func/invoker.rs index 4eb1f711..12accc9b 100644 --- a/azure-functions-codegen/src/func/invoker.rs +++ b/azure-functions-codegen/src/func/invoker.rs @@ -1,4 +1,4 @@ -use crate::func::{get_generic_argument_type, OutputBindings, CONTEXT_TYPE_NAME}; +use crate::func::{get_generic_argument_type, OutputBindings}; use azure_functions_shared::codegen::{bindings::TRIGGERS, last_segment_in_path}; use azure_functions_shared::util::to_camel_case; use proc_macro2::TokenStream; @@ -21,10 +21,24 @@ impl<'a> Invoker<'a> { } } + fn is_trigger_type(ty: &Type) -> bool { + match Invoker::deref_arg_type(ty) { + Type::Path(tp) => { + TRIGGERS.contains_key(last_segment_in_path(&tp.path).ident.to_string().as_str()) + } + Type::Paren(tp) => Invoker::is_trigger_type(&tp.elem), + _ => false, + } + } +} + +struct CommonInvokerTokens<'a>(pub &'a ItemFn); + +impl<'a> CommonInvokerTokens<'a> { fn get_input_args(&self) -> (Vec<&'a Ident>, Vec<&'a Type>) { self.iter_args() .filter_map(|(name, arg_type)| { - if Invoker::is_context_type(arg_type) | Invoker::is_trigger_type(arg_type) { + if Invoker::is_trigger_type(arg_type) { return None; } @@ -36,7 +50,7 @@ impl<'a> Invoker<'a> { fn get_input_assignments(&self) -> Vec { self.iter_args() .filter_map(|(_, arg_type)| { - if Invoker::is_context_type(arg_type) | Invoker::is_trigger_type(arg_type) { + if Invoker::is_trigger_type(arg_type) { return None; } @@ -66,13 +80,6 @@ impl<'a> Invoker<'a> { fn get_args_for_call(&self) -> Vec<::proc_macro2::TokenStream> { self.iter_args() .map(|(name, arg_type)| { - if Invoker::is_context_type(arg_type) { - if let Type::Reference(_) = arg_type { - return quote!(&__ctx) - } - return quote!(__ctx.clone()); - } - let name_str = name.to_string(); if let Type::Reference(tr) = arg_type { @@ -99,32 +106,10 @@ impl<'a> Invoker<'a> { _ => panic!("expected captured arguments"), }) } - - fn is_context_type(ty: &Type) -> bool { - match Invoker::deref_arg_type(ty) { - Type::Path(tp) => last_segment_in_path(&tp.path).ident == CONTEXT_TYPE_NAME, - Type::Paren(tp) => Invoker::is_context_type(&tp.elem), - _ => false, - } - } - - fn is_trigger_type(ty: &Type) -> bool { - match Invoker::deref_arg_type(ty) { - Type::Path(tp) => { - TRIGGERS.contains_key(last_segment_in_path(&tp.path).ident.to_string().as_str()) - } - Type::Paren(tp) => Invoker::is_trigger_type(&tp.elem), - _ => false, - } - } } -impl ToTokens for Invoker<'_> { +impl ToTokens for CommonInvokerTokens<'_> { fn to_tokens(&self, tokens: &mut ::proc_macro2::TokenStream) { - let invoker = Ident::new( - &format!("{}{}", INVOKER_PREFIX, self.0.ident.to_string()), - self.0.ident.span(), - ); let target = &self.0.ident; let (args, types) = self.get_input_args(); @@ -139,13 +124,7 @@ impl ToTokens for Invoker<'_> { let args_for_call = self.get_args_for_call(); - let output_bindings = OutputBindings(self.0); - - quote!(#[allow(dead_code)] - fn #invoker( - __name: &str, - __req: ::azure_functions::rpc::InvocationRequest, - ) -> ::azure_functions::rpc::InvocationResponse { + quote!( use azure_functions::{IntoVec, FromVec}; let mut #trigger_arg: Option<#trigger_type> = None; @@ -155,33 +134,87 @@ impl ToTokens for Invoker<'_> { for __param in __req.input_data.into_iter() { match __param.name.as_str() { - #trigger_name => #trigger_arg = Some( - #trigger_type::new( - __param.data.expect("expected parameter binding data"), - __metadata.take().expect("expected only one trigger") + #trigger_name => #trigger_arg = Some( + #trigger_type::new( + __param.data.expect("expected parameter binding data"), + __metadata.take().expect("expected only one trigger") ) ), - #(#arg_names => #args_for_match = Some(#arg_assignments),)* + #(#arg_names => #args_for_match = Some(#arg_assignments),)* _ => panic!(format!("unexpected parameter binding '{}'", __param.name)), }; } - let __ctx = ::azure_functions::Context::new(&__req.invocation_id, &__req.function_id, __name); let __ret = #target(#(#args_for_call,)*); + ) + .to_tokens(tokens); + } +} - let mut __res = ::azure_functions::rpc::InvocationResponse { - invocation_id: __req.invocation_id, - result: Some(::azure_functions::rpc::StatusResult { - status: ::azure_functions::rpc::status_result::Status::Success as i32, - ..Default::default() - }), - ..Default::default() - }; +impl ToTokens for Invoker<'_> { + fn to_tokens(&self, tokens: &mut ::proc_macro2::TokenStream) { + let ident = Ident::new( + &format!("{}{}", INVOKER_PREFIX, self.0.ident.to_string()), + self.0.ident.span(), + ); + + let common_tokens = CommonInvokerTokens(&self.0); - #output_bindings + let output_bindings = OutputBindings(self.0); + + if self.0.asyncness.is_some() { + quote!( + #[allow(dead_code)] + fn #ident( + __req: ::azure_functions::rpc::InvocationRequest, + ) -> ::azure_functions::codegen::InvocationFuture { + #common_tokens + + use futures::future::FutureExt; + + let __id = __req.invocation_id; + + Box::pin( + __ret.then(move |__ret| { + let mut __res = ::azure_functions::rpc::InvocationResponse { + invocation_id: __id, + result: Some(::azure_functions::rpc::StatusResult { + status: ::azure_functions::rpc::status_result::Status::Success as i32, + ..Default::default() + }), + ..Default::default() + }; + + #output_bindings + + ::futures::future::ready(__res) + }) + ) + } + ).to_tokens(tokens); + } else { + quote!( + #[allow(dead_code)] + fn #ident( + __req: ::azure_functions::rpc::InvocationRequest, + ) -> ::azure_functions::rpc::InvocationResponse { + #common_tokens + + let mut __res = ::azure_functions::rpc::InvocationResponse { + invocation_id: __req.invocation_id, + result: Some(::azure_functions::rpc::StatusResult { + status: ::azure_functions::rpc::status_result::Status::Success as i32, + ..Default::default() + }), + ..Default::default() + }; - __res + #output_bindings - }).to_tokens(tokens); + __res + } + ) + .to_tokens(tokens); + } } } diff --git a/azure-functions-shared/src/codegen.rs b/azure-functions-shared/src/codegen.rs index 5256687d..f5047b3b 100644 --- a/azure-functions-shared/src/codegen.rs +++ b/azure-functions-shared/src/codegen.rs @@ -95,3 +95,30 @@ where { panic!("{}", message.as_ref()); } + +#[cfg(test)] +mod tests { + use std::panic::{catch_unwind, UnwindSafe}; + + pub fn should_panic(callback: T, msg: &str) + where + T: FnOnce() + UnwindSafe, + { + let result = catch_unwind(|| callback()); + assert!(result.is_err(), "the function did not panic"); + + if cfg!(feature = "unstable") { + assert_eq!( + result.unwrap_err().downcast_ref::().unwrap(), + "aborting due to previous error", + "the panic message is not the expected one" + ); + } else { + assert_eq!( + result.unwrap_err().downcast_ref::().unwrap(), + msg, + "the panic message is not the expected one" + ); + } + } +} diff --git a/azure-functions-shared/src/codegen/bindings.rs b/azure-functions-shared/src/codegen/bindings.rs index 96a9fa2b..5a3c390e 100644 --- a/azure-functions-shared/src/codegen/bindings.rs +++ b/azure-functions-shared/src/codegen/bindings.rs @@ -368,30 +368,3 @@ lazy_static! { set }; } - -#[cfg(test)] -mod tests { - use std::panic::{catch_unwind, UnwindSafe}; - - pub fn should_panic(callback: T, msg: &str) - where - T: FnOnce() + UnwindSafe, - { - let result = catch_unwind(|| callback()); - assert!(result.is_err(), "the function did not panic"); - - if cfg!(feature = "unstable") { - assert_eq!( - result.unwrap_err().downcast_ref::().unwrap(), - "aborting due to previous error", - "the panic message is not the expected one" - ); - } else { - assert_eq!( - result.unwrap_err().downcast_ref::().unwrap(), - msg, - "the panic message is not the expected one" - ); - } - } -} diff --git a/azure-functions-shared/src/codegen/bindings/blob.rs b/azure-functions-shared/src/codegen/bindings/blob.rs index ab7a871e..7150b919 100644 --- a/azure-functions-shared/src/codegen/bindings/blob.rs +++ b/azure-functions-shared/src/codegen/bindings/blob.rs @@ -14,7 +14,7 @@ pub struct Blob { #[cfg(test)] mod tests { use super::*; - use crate::codegen::bindings::tests::should_panic; + use crate::codegen::tests::should_panic; use proc_macro2::{Span, TokenStream}; use quote::ToTokens; use serde_json::to_string; diff --git a/azure-functions-shared/src/codegen/bindings/blob_trigger.rs b/azure-functions-shared/src/codegen/bindings/blob_trigger.rs index 9260d359..b36da42b 100644 --- a/azure-functions-shared/src/codegen/bindings/blob_trigger.rs +++ b/azure-functions-shared/src/codegen/bindings/blob_trigger.rs @@ -14,7 +14,7 @@ pub struct BlobTrigger { #[cfg(test)] mod tests { use super::*; - use crate::codegen::bindings::tests::should_panic; + use crate::codegen::tests::should_panic; use proc_macro2::{Span, TokenStream}; use quote::ToTokens; use serde_json::to_string; diff --git a/azure-functions-shared/src/codegen/bindings/cosmos_db.rs b/azure-functions-shared/src/codegen/bindings/cosmos_db.rs index df5fc071..c9b2bcbb 100644 --- a/azure-functions-shared/src/codegen/bindings/cosmos_db.rs +++ b/azure-functions-shared/src/codegen/bindings/cosmos_db.rs @@ -32,7 +32,7 @@ pub struct CosmosDb { #[cfg(test)] mod tests { use super::*; - use crate::codegen::bindings::tests::should_panic; + use crate::codegen::tests::should_panic; use proc_macro2::{Span, TokenStream}; use quote::ToTokens; use serde_json::to_string; diff --git a/azure-functions-shared/src/codegen/bindings/cosmos_db_trigger.rs b/azure-functions-shared/src/codegen/bindings/cosmos_db_trigger.rs index 794e4895..47ea4120 100644 --- a/azure-functions-shared/src/codegen/bindings/cosmos_db_trigger.rs +++ b/azure-functions-shared/src/codegen/bindings/cosmos_db_trigger.rs @@ -42,7 +42,7 @@ pub struct CosmosDbTrigger { #[cfg(test)] mod tests { use super::*; - use crate::codegen::bindings::tests::should_panic; + use crate::codegen::tests::should_panic; use proc_macro2::{Span, TokenStream}; use quote::ToTokens; use serde_json::to_string; diff --git a/azure-functions-shared/src/codegen/bindings/event_grid_trigger.rs b/azure-functions-shared/src/codegen/bindings/event_grid_trigger.rs index f8433c28..7aab73ea 100644 --- a/azure-functions-shared/src/codegen/bindings/event_grid_trigger.rs +++ b/azure-functions-shared/src/codegen/bindings/event_grid_trigger.rs @@ -10,7 +10,7 @@ pub struct EventGridTrigger { #[cfg(test)] mod tests { use super::*; - use crate::codegen::bindings::tests::should_panic; + use crate::codegen::tests::should_panic; use proc_macro2::{Span, TokenStream}; use quote::ToTokens; use serde_json::to_string; diff --git a/azure-functions-shared/src/codegen/bindings/event_hub.rs b/azure-functions-shared/src/codegen/bindings/event_hub.rs index e9acb09d..88db5f54 100644 --- a/azure-functions-shared/src/codegen/bindings/event_hub.rs +++ b/azure-functions-shared/src/codegen/bindings/event_hub.rs @@ -13,7 +13,7 @@ pub struct EventHub { #[cfg(test)] mod tests { use super::*; - use crate::codegen::bindings::tests::should_panic; + use crate::codegen::tests::should_panic; use proc_macro2::{Span, TokenStream}; use quote::ToTokens; use serde_json::to_string; diff --git a/azure-functions-shared/src/codegen/bindings/event_hub_trigger.rs b/azure-functions-shared/src/codegen/bindings/event_hub_trigger.rs index 7df691fb..e82c493d 100644 --- a/azure-functions-shared/src/codegen/bindings/event_hub_trigger.rs +++ b/azure-functions-shared/src/codegen/bindings/event_hub_trigger.rs @@ -15,7 +15,7 @@ pub struct EventHubTrigger { #[cfg(test)] mod tests { use super::*; - use crate::codegen::bindings::tests::should_panic; + use crate::codegen::tests::should_panic; use proc_macro2::{Span, TokenStream}; use quote::ToTokens; use serde_json::to_string; diff --git a/azure-functions-shared/src/codegen/bindings/generic.rs b/azure-functions-shared/src/codegen/bindings/generic.rs index ee65472c..d529461d 100644 --- a/azure-functions-shared/src/codegen/bindings/generic.rs +++ b/azure-functions-shared/src/codegen/bindings/generic.rs @@ -129,7 +129,7 @@ impl ToTokens for Generic { #[cfg(test)] mod tests { use super::*; - use crate::codegen::bindings::tests::should_panic; + use crate::codegen::tests::should_panic; use proc_macro2::{Span, TokenStream}; use quote::ToTokens; use serde_json::to_string; diff --git a/azure-functions-shared/src/codegen/bindings/http.rs b/azure-functions-shared/src/codegen/bindings/http.rs index 9131e7dc..b2c34805 100644 --- a/azure-functions-shared/src/codegen/bindings/http.rs +++ b/azure-functions-shared/src/codegen/bindings/http.rs @@ -10,7 +10,7 @@ pub struct Http { #[cfg(test)] mod tests { use super::*; - use crate::codegen::bindings::tests::should_panic; + use crate::codegen::tests::should_panic; use proc_macro2::{Span, TokenStream}; use quote::ToTokens; use serde_json::to_string; diff --git a/azure-functions-shared/src/codegen/bindings/http_trigger.rs b/azure-functions-shared/src/codegen/bindings/http_trigger.rs index 00aa0585..cb54d1e0 100644 --- a/azure-functions-shared/src/codegen/bindings/http_trigger.rs +++ b/azure-functions-shared/src/codegen/bindings/http_trigger.rs @@ -15,7 +15,7 @@ pub struct HttpTrigger { #[cfg(test)] mod tests { use super::*; - use crate::codegen::bindings::tests::should_panic; + use crate::codegen::tests::should_panic; use proc_macro2::{Span, TokenStream}; use quote::ToTokens; use serde_json::to_string; diff --git a/azure-functions-shared/src/codegen/bindings/queue.rs b/azure-functions-shared/src/codegen/bindings/queue.rs index bd7d052d..00e1ba56 100644 --- a/azure-functions-shared/src/codegen/bindings/queue.rs +++ b/azure-functions-shared/src/codegen/bindings/queue.rs @@ -13,7 +13,7 @@ pub struct Queue { #[cfg(test)] mod tests { use super::*; - use crate::codegen::bindings::tests::should_panic; + use crate::codegen::tests::should_panic; use proc_macro2::{Span, TokenStream}; use quote::ToTokens; use serde_json::to_string; diff --git a/azure-functions-shared/src/codegen/bindings/queue_trigger.rs b/azure-functions-shared/src/codegen/bindings/queue_trigger.rs index 89577f0c..6f9c94b4 100644 --- a/azure-functions-shared/src/codegen/bindings/queue_trigger.rs +++ b/azure-functions-shared/src/codegen/bindings/queue_trigger.rs @@ -13,7 +13,7 @@ pub struct QueueTrigger { #[cfg(test)] mod tests { use super::*; - use crate::codegen::bindings::tests::should_panic; + use crate::codegen::tests::should_panic; use proc_macro2::{Span, TokenStream}; use quote::ToTokens; use serde_json::to_string; diff --git a/azure-functions-shared/src/codegen/bindings/send_grid.rs b/azure-functions-shared/src/codegen/bindings/send_grid.rs index 9a1ab241..2dd9630a 100644 --- a/azure-functions-shared/src/codegen/bindings/send_grid.rs +++ b/azure-functions-shared/src/codegen/bindings/send_grid.rs @@ -16,7 +16,7 @@ pub struct SendGrid { #[cfg(test)] mod tests { use super::*; - use crate::codegen::bindings::tests::should_panic; + use crate::codegen::tests::should_panic; use proc_macro2::{Span, TokenStream}; use quote::ToTokens; use serde_json::to_string; diff --git a/azure-functions-shared/src/codegen/bindings/service_bus.rs b/azure-functions-shared/src/codegen/bindings/service_bus.rs index c911b143..b9b59e37 100644 --- a/azure-functions-shared/src/codegen/bindings/service_bus.rs +++ b/azure-functions-shared/src/codegen/bindings/service_bus.rs @@ -30,7 +30,7 @@ impl ServiceBus { #[cfg(test)] mod tests { use super::*; - use crate::codegen::bindings::tests::should_panic; + use crate::codegen::tests::should_panic; use proc_macro2::{Span, TokenStream}; use quote::ToTokens; use serde_json::to_string; diff --git a/azure-functions-shared/src/codegen/bindings/service_bus_trigger.rs b/azure-functions-shared/src/codegen/bindings/service_bus_trigger.rs index 28859977..af4510ed 100644 --- a/azure-functions-shared/src/codegen/bindings/service_bus_trigger.rs +++ b/azure-functions-shared/src/codegen/bindings/service_bus_trigger.rs @@ -30,7 +30,7 @@ impl ServiceBusTrigger { #[cfg(test)] mod tests { use super::*; - use crate::codegen::bindings::tests::should_panic; + use crate::codegen::tests::should_panic; use proc_macro2::{Span, TokenStream}; use quote::ToTokens; use serde_json::to_string; diff --git a/azure-functions-shared/src/codegen/bindings/signalr.rs b/azure-functions-shared/src/codegen/bindings/signalr.rs index 3b013968..a44af693 100644 --- a/azure-functions-shared/src/codegen/bindings/signalr.rs +++ b/azure-functions-shared/src/codegen/bindings/signalr.rs @@ -14,7 +14,7 @@ pub struct SignalR { #[cfg(test)] mod tests { use super::*; - use crate::codegen::bindings::tests::should_panic; + use crate::codegen::tests::should_panic; use proc_macro2::{Span, TokenStream}; use quote::ToTokens; use serde_json::to_string; diff --git a/azure-functions-shared/src/codegen/bindings/signalr_connection_info.rs b/azure-functions-shared/src/codegen/bindings/signalr_connection_info.rs index 7d19a99c..d4b547e7 100644 --- a/azure-functions-shared/src/codegen/bindings/signalr_connection_info.rs +++ b/azure-functions-shared/src/codegen/bindings/signalr_connection_info.rs @@ -16,7 +16,7 @@ pub struct SignalRConnectionInfo { #[cfg(test)] mod tests { use super::*; - use crate::codegen::bindings::tests::should_panic; + use crate::codegen::tests::should_panic; use proc_macro2::{Span, TokenStream}; use quote::ToTokens; use serde_json::to_string; diff --git a/azure-functions-shared/src/codegen/bindings/table.rs b/azure-functions-shared/src/codegen/bindings/table.rs index 9d69c57b..eb4f600f 100644 --- a/azure-functions-shared/src/codegen/bindings/table.rs +++ b/azure-functions-shared/src/codegen/bindings/table.rs @@ -21,7 +21,7 @@ pub struct Table { #[cfg(test)] mod tests { use super::*; - use crate::codegen::bindings::tests::should_panic; + use crate::codegen::tests::should_panic; use proc_macro2::{Span, TokenStream}; use quote::ToTokens; use serde_json::to_string; diff --git a/azure-functions-shared/src/codegen/bindings/timer_trigger.rs b/azure-functions-shared/src/codegen/bindings/timer_trigger.rs index 1d6112b3..67e1d212 100644 --- a/azure-functions-shared/src/codegen/bindings/timer_trigger.rs +++ b/azure-functions-shared/src/codegen/bindings/timer_trigger.rs @@ -15,7 +15,7 @@ pub struct TimerTrigger { #[cfg(test)] mod tests { use super::*; - use crate::codegen::bindings::tests::should_panic; + use crate::codegen::tests::should_panic; use proc_macro2::{Span, TokenStream}; use quote::ToTokens; use serde_json::to_string; diff --git a/azure-functions-shared/src/codegen/bindings/twilio_sms.rs b/azure-functions-shared/src/codegen/bindings/twilio_sms.rs index 8c503ffb..d05ecd4f 100644 --- a/azure-functions-shared/src/codegen/bindings/twilio_sms.rs +++ b/azure-functions-shared/src/codegen/bindings/twilio_sms.rs @@ -16,7 +16,7 @@ pub struct TwilioSms { #[cfg(test)] mod tests { use super::*; - use crate::codegen::bindings::tests::should_panic; + use crate::codegen::tests::should_panic; use proc_macro2::{Span, TokenStream}; use quote::ToTokens; use serde_json::to_string; diff --git a/azure-functions-shared/src/codegen/function.rs b/azure-functions-shared/src/codegen/function.rs index d7f109db..6e066bf1 100644 --- a/azure-functions-shared/src/codegen/function.rs +++ b/azure-functions-shared/src/codegen/function.rs @@ -10,14 +10,70 @@ use serde::{ser::SerializeMap, Serialize, Serializer}; use std::borrow::Cow; use syn::{parse_str, spanned::Spanned, AttributeArgs, Ident}; -#[doc(hidden)] -#[derive(Clone)] +pub type SyncFn = fn(rpc::InvocationRequest) -> rpc::InvocationResponse; + +#[cfg(feature = "unstable")] +pub type InvocationFuture = + std::pin::Pin + Send>>; + +#[cfg(feature = "unstable")] +pub type AsyncFn = fn(rpc::InvocationRequest) -> InvocationFuture; + +#[cfg(not(feature = "unstable"))] +pub type AsyncFn = fn(rpc::InvocationRequest) -> !; + +pub enum InvokerFn { + Sync(Option), + Async(Option), +} + +struct InvokerFnTokens<'a> { + ident: Ident, + invoker_fn: &'a InvokerFn, +} + +impl<'a> InvokerFnTokens<'a> { + pub fn new(name: &str, invoker_fn: &'a InvokerFn) -> Self { + InvokerFnTokens { + ident: Ident::new(name, Span::call_site()), + invoker_fn, + } + } +} + +impl<'a> ToTokens for InvokerFnTokens<'a> { + fn to_tokens(&self, tokens: &mut TokenStream) { + let ident = &self.ident; + match self.invoker_fn { + InvokerFn::Sync(_) => quote!(::azure_functions::codegen::InvokerFn::Sync(Some(#ident))), + InvokerFn::Async(_) => { + quote!(::azure_functions::codegen::InvokerFn::Async(Some(#ident))) + } + } + .to_tokens(tokens); + } +} + +pub struct Invoker { + pub name: Cow<'static, str>, + pub invoker_fn: InvokerFn, +} + +impl ToTokens for Invoker { + fn to_tokens(&self, tokens: &mut TokenStream) { + let name = QuotableBorrowedStr(&self.name); + let invoker_fn = InvokerFnTokens::new(&self.name, &self.invoker_fn); + + quote!(::azure_functions::codegen::Invoker { name: #name, invoker_fn: #invoker_fn, }) + .to_tokens(tokens); + } +} + pub struct Function { pub name: Cow<'static, str>, pub disabled: bool, pub bindings: Cow<'static, [Binding]>, - pub invoker_name: Option>, - pub invoker: Option rpc::InvocationResponse>, + pub invoker: Option, pub manifest_dir: Option>, pub file: Option>, } @@ -76,7 +132,6 @@ impl From for Function { name: name.unwrap_or(Cow::Borrowed("")), disabled: disabled.unwrap_or(false), bindings: Cow::Owned(Vec::new()), - invoker_name: None, invoker: None, manifest_dir: None, file: None, @@ -89,26 +144,140 @@ impl ToTokens for Function { let name = QuotableBorrowedStr(&self.name); let disabled = self.disabled; let bindings = self.bindings.iter().filter(|x| !x.is_context()); - let invoker_name = - QuotableOption(self.invoker_name.as_ref().map(|x| QuotableBorrowedStr(x))); - let invoker = Ident::new( - self.invoker_name - .as_ref() - .expect("function must have an invoker"), - Span::call_site(), - ); + let invoker = QuotableOption(self.invoker.as_ref()); quote!( - ::azure_functions::codegen::Function { - name: #name, - disabled: #disabled, - bindings: ::std::borrow::Cow::Borrowed(&[#(#bindings),*]), - invoker_name: #invoker_name, - invoker: Some(#invoker), - manifest_dir: Some(::std::borrow::Cow::Borrowed(env!("CARGO_MANIFEST_DIR"))), - file: Some(::std::borrow::Cow::Borrowed(file!())), - } + ::azure_functions::codegen::Function { + name: #name, + disabled: #disabled, + bindings: ::std::borrow::Cow::Borrowed(&[#(#bindings),*]), + invoker: #invoker, + manifest_dir: Some(::std::borrow::Cow::Borrowed(env!("CARGO_MANIFEST_DIR"))), + file: Some(::std::borrow::Cow::Borrowed(file!())), + } ) .to_tokens(tokens) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::codegen::{ + bindings::{Binding, Http, HttpTrigger}, + tests::should_panic, + }; + use proc_macro2::TokenStream; + use quote::ToTokens; + use serde_json::to_string; + use syn::{parse_str, NestedMeta}; + + #[test] + fn it_serializes_to_json() { + let func = Function { + name: Cow::from("name"), + disabled: false, + bindings: Cow::Owned(vec![ + Binding::HttpTrigger(HttpTrigger { + name: Cow::from("foo"), + auth_level: Some(Cow::from("bar")), + methods: Cow::from(vec![Cow::from("foo"), Cow::from("bar"), Cow::from("baz")]), + route: Some(Cow::from("baz")), + }), + Binding::Http(Http { + name: Cow::from("bar"), + }), + ]), + invoker: Some(Invoker { + name: Cow::Borrowed("invoker"), + invoker_fn: InvokerFn::Async(None), + }), + manifest_dir: None, + file: None, + }; + + assert_eq!( + to_string(&func).unwrap(), + r#"{"generatedBy":"azure-functions-rs","disabled":false,"bindings":[{"type":"httpTrigger","direction":"in","name":"foo","authLevel":"bar","methods":["foo","bar","baz"],"route":"baz"},{"type":"http","direction":"out","name":"bar"}]}"# + ); + } + + #[test] + fn it_parses_attribute_arguments() { + let func: Function = vec![ + parse_str::(r#"name = "foo""#).unwrap(), + parse_str::(r#"disabled = true"#).unwrap(), + ] + .into(); + + assert_eq!(func.name, "foo"); + assert_eq!(func.disabled, true); + assert_eq!(func.bindings.len(), 0); + assert_eq!(func.invoker.is_none(), true); + assert_eq!(func.manifest_dir.is_none(), true); + assert_eq!(func.file.is_none(), true); + } + + #[test] + fn it_requires_an_identifier_for_name() { + should_panic( + || { + let _: Function = vec![parse_str::(r#"name = "123""#).unwrap()].into(); + }, + "a legal function identifier is required for the \'name\' argument", + ); + } + + #[test] + fn it_requires_the_name_attribute_be_a_string() { + should_panic( + || { + let _: Function = vec![parse_str::(r#"name = false"#).unwrap()].into(); + }, + "expected a literal string value for the 'name' argument", + ); + } + + #[test] + fn it_requires_the_disabled_attribute_be_a_boolean() { + should_panic( + || { + let _: Function = + vec![parse_str::(r#"disabled = "false""#).unwrap()].into(); + }, + "expected a literal boolean value for the 'disabled' argument", + ); + } + + #[test] + fn it_converts_to_tokens() { + let func = Function { + name: Cow::from("name"), + disabled: false, + bindings: Cow::Owned(vec![ + Binding::HttpTrigger(HttpTrigger { + name: Cow::from("foo"), + auth_level: Some(Cow::from("bar")), + methods: Cow::from(vec![Cow::from("foo"), Cow::from("bar"), Cow::from("baz")]), + route: Some(Cow::from("baz")), + }), + Binding::Http(Http { + name: Cow::from("bar"), + }), + ]), + invoker: Some(Invoker { + name: Cow::Borrowed("invoker"), + invoker_fn: InvokerFn::Async(None), + }), + manifest_dir: None, + file: None, + }; + + let mut stream = TokenStream::new(); + func.to_tokens(&mut stream); + let mut tokens = stream.to_string(); + tokens.retain(|c| c != ' '); + + assert_eq!(tokens, r#"::azure_functions::codegen::Function{name:::std::borrow::Cow::Borrowed("name"),disabled:false,bindings:::std::borrow::Cow::Borrowed(&[::azure_functions::codegen::bindings::Binding::HttpTrigger(::azure_functions::codegen::bindings::HttpTrigger{name:::std::borrow::Cow::Borrowed("foo"),auth_level:Some(::std::borrow::Cow::Borrowed("bar")),methods:::std::borrow::Cow::Borrowed(&[::std::borrow::Cow::Borrowed("foo"),::std::borrow::Cow::Borrowed("bar"),::std::borrow::Cow::Borrowed("baz"),]),route:Some(::std::borrow::Cow::Borrowed("baz")),}),::azure_functions::codegen::bindings::Binding::Http(::azure_functions::codegen::bindings::Http{name:::std::borrow::Cow::Borrowed("bar"),})]),invoker:Some(::azure_functions::codegen::Invoker{name:::std::borrow::Cow::Borrowed("invoker"),invoker_fn:::azure_functions::codegen::InvokerFn::Async(Some(invoker)),}),manifest_dir:Some(::std::borrow::Cow::Borrowed(env!("CARGO_MANIFEST_DIR"))),file:Some(::std::borrow::Cow::Borrowed(file!())),}"#); + } +} diff --git a/azure-functions-shared/src/context.rs b/azure-functions-shared/src/context.rs deleted file mode 100644 index 7029524c..00000000 --- a/azure-functions-shared/src/context.rs +++ /dev/null @@ -1,49 +0,0 @@ -use std::env; -use std::path::PathBuf; - -/// Represents context about an Azure Function invocation. -#[derive(Debug, Clone)] -pub struct Context<'a> { - invocation_id: &'a str, - function_id: &'a str, - name: &'a str, -} - -impl<'a> Context<'a> { - /// Creates a new function invocation context. - pub fn new(invocation_id: &'a str, function_id: &'a str, name: &'a str) -> Self { - Context { - invocation_id, - function_id, - name, - } - } - - /// Gets the invocation identifier for the current Azure Function. - pub fn invocation_id(&self) -> &str { - self.invocation_id - } - - /// Gets the function identifier for the current Azure Function. - pub fn function_id(&self) -> &str { - self.function_id - } - - /// Gets the name of the current Azure Function. - pub fn function_name(&self) -> &str { - self.name - } - - /// Gets the directory for the current Azure Function. - pub fn function_directory(&self) -> Option { - self.app_directory().map(|p| p.join(self.name)) - } - - /// Gets the directory for the current Azure Function Application. - pub fn app_directory(&self) -> Option { - env::current_exe() - .map(|p| p.parent().map(ToOwned::to_owned)) - .ok() - .unwrap_or(None) - } -} diff --git a/azure-functions-shared/src/lib.rs b/azure-functions-shared/src/lib.rs index f02632fb..8cec5288 100644 --- a/azure-functions-shared/src/lib.rs +++ b/azure-functions-shared/src/lib.rs @@ -9,7 +9,6 @@ #[doc(hidden)] pub mod codegen; -mod context; #[doc(hidden)] pub mod util; @@ -22,5 +21,3 @@ pub mod rpc { pub use self::azure_functions_rpc_messages::*; } - -pub use self::context::*; diff --git a/azure-functions/Cargo.toml b/azure-functions/Cargo.toml index 10b0e800..650898ea 100644 --- a/azure-functions/Cargo.toml +++ b/azure-functions/Cargo.toml @@ -17,9 +17,11 @@ tower-grpc = "0.1.0" tower-service = "0.2.0" tower-util = "0.1.0" log = { version = "0.4.6", features = ["std"] } -futures = "0.1.28" +futures01 = { package = "futures", version = "0.1.28" } +futures-preview = { version = "0.3.0-alpha.17", features = ["compat"], optional = true } clap = "2.33.0" tokio = "0.1.22" +tokio-threadpool = "0.1.15" serde = "1.0.94" serde_json = "1.0.40" serde_derive = "1.0.94" @@ -33,7 +35,7 @@ fs_extra = "1.1.0" semver = "0.9.0" [features] -unstable = ["azure-functions-codegen/unstable", "azure-functions-shared/unstable"] +unstable = ["azure-functions-codegen/unstable", "azure-functions-shared/unstable", "futures-preview"] [dev-dependencies] matches = "0.1.8" diff --git a/azure-functions/src/commands/run.rs b/azure-functions/src/commands/run.rs index 0e3242c2..7aa3ffb1 100644 --- a/azure-functions/src/commands/run.rs +++ b/azure-functions/src/commands/run.rs @@ -1,82 +1,5 @@ -use crate::{ - backtrace::Backtrace, - codegen::Function, - logger, - registry::Registry, - rpc::{ - client::FunctionRpc, status_result::Status, streaming_message::Content, - FunctionLoadRequest, FunctionLoadResponse, InvocationRequest, InvocationResponse, - StartStream, StatusResult, StreamingMessage, WorkerInitResponse, WorkerStatusRequest, - WorkerStatusResponse, - }, -}; +use crate::{registry::Registry, worker::Worker}; use clap::{App, Arg, ArgMatches, SubCommand}; -use futures::{future::lazy, sync::mpsc::unbounded, Future, Poll, Stream}; -use http::{ - uri::{Authority, Parts, Scheme, Uri}, - Request as HttpRequest, -}; -use log::error; -use std::cell::RefCell; -use std::panic::{catch_unwind, set_hook, AssertUnwindSafe, PanicInfo}; -use tower_grpc::Request; -use tower_hyper::{ - client::Builder as HttpBuilder, - util::{Connector, Destination, HttpConnector}, - Connect, -}; -use tower_service::Service; -use tower_util::MakeService; - -const UNKNOWN: &str = ""; - -thread_local!(static FUNCTION_NAME: RefCell<&'static str> = RefCell::new(UNKNOWN)); - -type Sender = futures::sync::mpsc::UnboundedSender; - -// TODO: replace with tower-request-modifier when published (see: https://github.com/tower-rs/tower-http/issues/24) -struct HttpOriginService { - inner: T, - scheme: Scheme, - authority: Authority, -} - -impl HttpOriginService { - pub fn new(inner: T, uri: Uri) -> Self { - let parts = Parts::from(uri); - - HttpOriginService { - inner, - scheme: parts.scheme.unwrap(), - authority: parts.authority.unwrap(), - } - } -} - -impl Service> for HttpOriginService -where - T: Service>, -{ - type Response = T::Response; - type Error = T::Error; - type Future = T::Future; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - self.inner.poll_ready() - } - - fn call(&mut self, req: HttpRequest) -> Self::Future { - let (mut head, body) = req.into_parts(); - let mut parts = Parts::from(head.uri); - - parts.authority = Some(self.authority.clone()); - parts.scheme = Some(self.scheme.clone()); - - head.uri = Uri::from_parts(parts).expect("valid uri"); - - self.inner.call(HttpRequest::from_parts(head, body)) - } -} pub struct Run<'a> { pub host: &'a str, @@ -125,280 +48,13 @@ impl<'a> Run<'a> { ) } - pub fn execute(&self, mut registry: Registry<'static>) -> Result<(), String> { + pub fn execute(&self, registry: Registry<'static>) -> Result<(), String> { ctrlc::set_handler(|| {}).expect("failed setting SIGINT handler"); - let host_uri: Uri = format!("http://{0}:{1}", self.host, self.port) - .parse() - .unwrap(); - let (sender, receiver) = unbounded::(); - - // Start by sending a start stream message to the channel - // This will be sent to the host upon connection - sender - .unbounded_send(StreamingMessage { - content: Some(Content::StartStream(StartStream { - worker_id: self.worker_id.to_owned(), - })), - ..Default::default() - }) - .unwrap(); - - println!("Connecting to Azure Functions host at {0}", host_uri); - - let run = Connect::with_builder( - Connector::new(HttpConnector::new(1)), - HttpBuilder::new().http2_only(true).clone(), - ) - .make_service(Destination::try_from_uri(host_uri.clone()).unwrap()) - .map(move |conn| FunctionRpc::new(HttpOriginService::new(conn, host_uri))) - .map_err(|e| panic!("failed to connect to host: {}", e)) - .and_then(|mut client| { - client - .event_stream(Request::new( - receiver.map_err(|_| panic!("failed to receive from channel")), - )) - .map_err(|e| panic!("failed to start event stream: {}", e)) - }) - .and_then(move |stream| { - stream - .into_inner() - .into_future() - .map_err(|(e, _)| panic!("failed to read worker init request: {}", e)) - .and_then(move |(init_req, stream)| { - Run::handle_worker_init_request( - sender.clone(), - init_req.expect("expected a worker init request"), - ); - - stream - .for_each(move |req| { - Run::handle_request(&mut registry, sender.clone(), req); - Ok(()) - }) - .map_err(|e| panic!("fail to read request: {}", e)) - }) - }); - - tokio::run(run); + Worker::run(self.host, self.port, self.worker_id, registry); Ok(()) } - - fn handle_worker_init_request(sender: Sender, req: StreamingMessage) { - match req.content { - Some(Content::WorkerInitRequest(req)) => { - println!( - "Connected to Azure Functions host version {}.", - req.host_version - ); - - // TODO: use the level requested by the Azure functions host - log::set_boxed_logger(Box::new(logger::Logger::new( - log::Level::Info, - sender.clone(), - ))) - .expect("failed to set the global logger instance"); - - set_hook(Box::new(Run::handle_panic)); - - log::set_max_level(log::LevelFilter::Trace); - - sender - .unbounded_send(StreamingMessage { - content: Some(Content::WorkerInitResponse(WorkerInitResponse { - worker_version: env!("CARGO_PKG_VERSION").to_owned(), - result: Some(StatusResult { - status: Status::Success as i32, - ..Default::default() - }), - ..Default::default() - })), - ..Default::default() - }) - .unwrap(); - } - _ => panic!("expected a worker init request message from the host"), - }; - } - - fn handle_request(registry: &mut Registry<'static>, sender: Sender, req: StreamingMessage) { - match req.content { - Some(Content::FunctionLoadRequest(req)) => { - Run::handle_function_load_request(registry, sender, req) - } - Some(Content::InvocationRequest(req)) => { - Run::handle_invocation_request(registry, sender, req) - } - Some(Content::WorkerStatusRequest(req)) => { - Run::handle_worker_status_request(sender, req) - } - Some(Content::FileChangeEventRequest(_)) => {} - Some(Content::InvocationCancel(_)) => {} - Some(Content::FunctionEnvironmentReloadRequest(_)) => {} - _ => panic!("unexpected message from host: {:?}.", req), - }; - } - - fn handle_function_load_request( - registry: &mut Registry<'static>, - sender: Sender, - req: FunctionLoadRequest, - ) { - let mut result = StatusResult::default(); - - match req.metadata.as_ref() { - Some(metadata) => { - if registry.register(&req.function_id, &metadata.name) { - result.status = Status::Success as i32; - } else { - result.status = Status::Failure as i32; - result.result = format!("Function '{}' does not exist.", metadata.name); - } - } - None => { - result.status = Status::Failure as i32; - result.result = "Function load request metadata is missing.".to_string(); - } - }; - - sender - .unbounded_send(StreamingMessage { - content: Some(Content::FunctionLoadResponse(FunctionLoadResponse { - function_id: req.function_id, - result: Some(result), - ..Default::default() - })), - ..Default::default() - }) - .expect("failed to send function load response"); - } - - fn handle_invocation_request( - registry: &Registry<'static>, - sender: Sender, - req: InvocationRequest, - ) { - if let Some(func) = registry.get(&req.function_id) { - tokio::spawn(lazy(move || { - Run::invoke_function(func, sender, req); - Ok(()) - })); - return; - } - - let error = format!("Function with id '{}' does not exist.", req.function_id); - - sender - .unbounded_send(StreamingMessage { - content: Some(Content::InvocationResponse(InvocationResponse { - invocation_id: req.invocation_id, - result: Some(StatusResult { - status: Status::Failure as i32, - result: error, - ..Default::default() - }), - ..Default::default() - })), - ..Default::default() - }) - .expect("failed to send invocation response"); - } - - fn handle_worker_status_request(sender: Sender, _: WorkerStatusRequest) { - sender - .unbounded_send(StreamingMessage { - content: Some(Content::WorkerStatusResponse(WorkerStatusResponse {})), - ..Default::default() - }) - .expect("failed to send worker status response"); - } - - fn invoke_function(func: &'static Function, sender: Sender, req: InvocationRequest) { - // Set the function name in TLS - FUNCTION_NAME.with(|n| { - *n.borrow_mut() = &func.name; - }); - - // Set the invocation ID in TLS - logger::INVOCATION_ID.with(|id| { - id.borrow_mut().replace_range(.., &req.invocation_id); - }); - - let response = match catch_unwind(AssertUnwindSafe(|| { - (func - .invoker - .as_ref() - .expect("function must have an invoker"))(&func.name, req) - })) { - Ok(res) => res, - Err(_) => logger::INVOCATION_ID.with(|id| InvocationResponse { - invocation_id: id.borrow().clone(), - result: Some(StatusResult { - status: Status::Failure as i32, - result: "Azure Function panicked: see log for more information.".to_string(), - ..Default::default() - }), - ..Default::default() - }), - }; - - // Clear the function name from TLS - FUNCTION_NAME.with(|n| { - *n.borrow_mut() = UNKNOWN; - }); - - // Clear the invocation ID from TLS - logger::INVOCATION_ID.with(|id| { - id.borrow_mut().clear(); - }); - - sender - .unbounded_send(StreamingMessage { - content: Some(Content::InvocationResponse(response)), - ..Default::default() - }) - .expect("failed to send invocation response"); - } - - fn handle_panic(info: &PanicInfo) { - let backtrace = Backtrace::new(); - match info.location() { - Some(location) => { - error!( - "Azure Function '{}' panicked with '{}', {}:{}:{}{}", - FUNCTION_NAME.with(|f| *f.borrow()), - info.payload() - .downcast_ref::<&str>() - .cloned() - .unwrap_or_else(|| info - .payload() - .downcast_ref::() - .map(String::as_str) - .unwrap_or(UNKNOWN)), - location.file(), - location.line(), - location.column(), - backtrace - ); - } - None => { - error!( - "Azure Function '{}' panicked with '{}'{}", - FUNCTION_NAME.with(|f| *f.borrow()), - info.payload() - .downcast_ref::<&str>() - .cloned() - .unwrap_or_else(|| info - .payload() - .downcast_ref::() - .map(String::as_str) - .unwrap_or(UNKNOWN)), - backtrace - ); - } - }; - } } impl<'a> From<&'a ArgMatches<'a>> for Run<'a> { diff --git a/azure-functions/src/context.rs b/azure-functions/src/context.rs new file mode 100644 index 00000000..706f1349 --- /dev/null +++ b/azure-functions/src/context.rs @@ -0,0 +1,120 @@ +//! Module for function invocation context. +use std::{cell::RefCell, env, path::PathBuf}; + +pub(crate) const UNKNOWN_FUNCTION: &str = ""; + +thread_local!(pub(crate) static CURRENT: RefCell = RefCell::new( + Context{ + invocation_id: String::new(), + function_id: String::new(), + function_name: UNKNOWN_FUNCTION + } +)); + +/// Represents context about an Azure Function invocation. +#[derive(Debug, Clone)] +pub struct Context { + pub(crate) invocation_id: String, + pub(crate) function_id: String, + pub(crate) function_name: &'static str, +} + +pub(crate) struct ContextGuard; + +impl Drop for ContextGuard { + fn drop(&mut self) { + Context::clear(); + } +} + +impl Context { + /// Gets the current invocation context. + /// + /// Returns None if there is no invocation context. + pub fn current() -> Option { + let mut current = None; + + CURRENT.with(|c| { + let c = c.borrow(); + if !c.invocation_id.is_empty() { + current = Some(c.clone()); + } + }); + + current + } + + #[must_use] + pub(crate) fn set( + invocation_id: &str, + function_id: &str, + function_name: &'static str, + ) -> ContextGuard { + CURRENT.with(|c| { + let mut c = c.borrow_mut(); + c.invocation_id.replace_range(.., invocation_id); + c.function_id.replace_range(.., function_id); + c.function_name = function_name; + }); + + ContextGuard {} + } + + pub(crate) fn clear() { + CURRENT.with(|c| { + let mut c = c.borrow_mut(); + c.invocation_id.clear(); + c.function_id.clear(); + c.function_name = UNKNOWN_FUNCTION; + }); + } + + /// Gets the invocation identifier for the current Azure Function. + pub fn invocation_id(&self) -> &str { + &self.invocation_id + } + + /// Gets the function identifier for the current Azure Function. + pub fn function_id(&self) -> &str { + &self.function_id + } + + /// Gets the name of the current Azure Function. + pub fn function_name(&self) -> &str { + self.function_name + } + + /// Gets the directory for the current Azure Function. + pub fn function_directory(&self) -> Option { + self.app_directory().map(|p| p.join(self.function_name)) + } + + /// Gets the directory for the current Azure Function Application. + pub fn app_directory(&self) -> Option { + env::current_exe() + .map(|p| p.parent().map(ToOwned::to_owned)) + .ok() + .unwrap_or(None) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_returns_none_without_context() { + assert_eq!(Context::current().is_none(), true); + } + + #[test] + fn it_returns_current_context() { + let _guard = Context::set("1234", "5678", "foo"); + + let context = Context::current().unwrap(); + + assert_eq!(context.invocation_id(), "1234"); + assert_eq!(context.function_id(), "5678"); + assert_eq!(context.function_name(), "foo"); + } +} diff --git a/azure-functions/src/lib.rs b/azure-functions/src/lib.rs index 79a00097..fa5eff6a 100644 --- a/azure-functions/src/lib.rs +++ b/azure-functions/src/lib.rs @@ -99,9 +99,11 @@ mod commands; mod logger; mod registry; mod util; +mod worker; pub mod bindings; pub mod blob; +pub mod context; pub mod event_hub; pub mod generic; pub mod http; @@ -110,7 +112,7 @@ pub mod signalr; pub mod timer; #[doc(no_inline)] pub use azure_functions_codegen::export; -pub use azure_functions_shared::{rpc, Context}; +pub use azure_functions_shared::rpc; use crate::commands::{Init, Run, SyncExtensions}; use crate::registry::Registry; diff --git a/azure-functions/src/logger.rs b/azure-functions/src/logger.rs index cd209ea2..f74970b1 100644 --- a/azure-functions/src/logger.rs +++ b/azure-functions/src/logger.rs @@ -1,10 +1,8 @@ -use crate::rpc::{rpc_log, streaming_message::Content, RpcLog, StreamingMessage}; +use crate::{ + rpc::{rpc_log, streaming_message::Content, RpcLog, StreamingMessage}, + worker::Sender, +}; use log::{Level, Log, Metadata, Record}; -use std::cell::RefCell; - -thread_local!(pub static INVOCATION_ID: RefCell = RefCell::new(String::new())); - -type Sender = futures::sync::mpsc::UnboundedSender; pub struct Logger { level: Level, @@ -39,12 +37,7 @@ impl Log for Logger { ..Default::default() }; - INVOCATION_ID.with(|id| { - let id = id.borrow(); - if !id.is_empty() { - event.invocation_id = id.clone(); - } - }); + event.invocation_id = crate::context::CURRENT.with(|c| c.borrow().invocation_id.clone()); self.sender .unbounded_send(StreamingMessage { diff --git a/azure-functions/src/registry.rs b/azure-functions/src/registry.rs index d5453e5b..93dd8059 100644 --- a/azure-functions/src/registry.rs +++ b/azure-functions/src/registry.rs @@ -185,7 +185,6 @@ mod tests { name: Cow::Borrowed("function1"), disabled: false, bindings: Cow::Borrowed(&[]), - invoker_name: None, invoker: None, manifest_dir: None, file: None, @@ -194,7 +193,6 @@ mod tests { name: Cow::Borrowed("function2"), disabled: false, bindings: Cow::Borrowed(&[]), - invoker_name: None, invoker: None, manifest_dir: None, file: None, @@ -203,7 +201,6 @@ mod tests { name: Cow::Borrowed("function3"), disabled: false, bindings: Cow::Borrowed(&[]), - invoker_name: None, invoker: None, manifest_dir: None, file: None, @@ -221,7 +218,6 @@ mod tests { name: Cow::Borrowed("function1"), disabled: false, bindings: Cow::Borrowed(&[]), - invoker_name: None, invoker: None, manifest_dir: None, file: None, @@ -242,7 +238,6 @@ mod tests { name: Cow::Borrowed("function1"), disabled: false, bindings: Cow::Borrowed(&[]), - invoker_name: None, invoker: None, manifest_dir: None, file: None, @@ -273,7 +268,6 @@ mod tests { direction: Direction::Out, }), ]), - invoker_name: None, invoker: None, manifest_dir: None, file: None, @@ -297,7 +291,6 @@ mod tests { queue_name: Cow::Borrowed("some_queue"), connection: None, })]), - invoker_name: None, invoker: None, manifest_dir: None, file: None, @@ -317,7 +310,6 @@ mod tests { bindings: Cow::Borrowed(&[Binding::Http(bindings::Http { name: Cow::Borrowed("binding1"), })]), - invoker_name: None, invoker: None, manifest_dir: None, file: None, diff --git a/azure-functions/src/worker.rs b/azure-functions/src/worker.rs new file mode 100644 index 00000000..e3a62903 --- /dev/null +++ b/azure-functions/src/worker.rs @@ -0,0 +1,425 @@ +use crate::{ + backtrace::Backtrace, + codegen::{AsyncFn, Function, InvokerFn}, + context::Context, + logger, + registry::Registry, + rpc::{ + client::FunctionRpc, status_result::Status, streaming_message::Content, + FunctionLoadRequest, FunctionLoadResponse, InvocationRequest, InvocationResponse, + StartStream, StatusResult, StreamingMessage, WorkerInitResponse, WorkerStatusRequest, + WorkerStatusResponse, + }, +}; +use http::{ + uri::{Authority, Parts, Scheme, Uri}, + Request as HttpRequest, +}; +use log::error; +use std::cell::RefCell; +use std::panic::{catch_unwind, set_hook, AssertUnwindSafe, PanicInfo}; +use tokio_threadpool::blocking; +use tower_grpc::Request; +use tower_hyper::{ + client::Builder as HttpBuilder, + util::{Connector, Destination, HttpConnector}, + Connect, +}; +use tower_service::Service; +use tower_util::MakeService; + +use futures01::{future::poll_fn, sync::mpsc::unbounded, Async, Future, Poll, Stream}; + +pub type Sender = futures01::sync::mpsc::UnboundedSender; + +// TODO: replace with tower-request-modifier when published (see: https://github.com/tower-rs/tower-http/issues/24) +struct HttpOriginService { + inner: T, + scheme: Scheme, + authority: Authority, +} + +impl HttpOriginService { + pub fn new(inner: T, uri: Uri) -> Self { + let parts = Parts::from(uri); + + HttpOriginService { + inner, + scheme: parts.scheme.unwrap(), + authority: parts.authority.unwrap(), + } + } +} + +impl Service> for HttpOriginService +where + T: Service>, +{ + type Response = T::Response; + type Error = T::Error; + type Future = T::Future; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + self.inner.poll_ready() + } + + fn call(&mut self, req: HttpRequest) -> Self::Future { + let (mut head, body) = req.into_parts(); + let mut parts = Parts::from(head.uri); + + parts.authority = Some(self.authority.clone()); + parts.scheme = Some(self.scheme.clone()); + + head.uri = Uri::from_parts(parts).expect("valid uri"); + + self.inner.call(HttpRequest::from_parts(head, body)) + } +} + +struct ContextFuture { + inner: F, + invocation_id: String, + function_id: String, + function_name: &'static str, + sender: Sender, +} + +impl ContextFuture { + pub fn new( + inner: F, + invocation_id: String, + function_id: String, + function_name: &'static str, + sender: Sender, + ) -> Self { + ContextFuture { + inner, + invocation_id, + function_id, + function_name, + sender, + } + } +} + +// TODO: migrate this to std::future::Future when Tokio supports it +impl> Future for ContextFuture { + type Item = (); + type Error = F::Error; + + fn poll(&mut self) -> Poll { + let _guard = Context::set(&self.invocation_id, &self.function_id, self.function_name); + + let res = match catch_unwind(AssertUnwindSafe(|| self.inner.poll())) { + Ok(p) => match p? { + Async::Ready(res) => res, + Async::NotReady => return Ok(Async::NotReady), + }, + Err(_) => InvocationResponse { + invocation_id: self.invocation_id.clone(), + result: Some(StatusResult { + status: Status::Failure as i32, + result: "Azure Function panicked: see log for more information.".to_string(), + ..Default::default() + }), + ..Default::default() + }, + }; + + self.sender + .unbounded_send(StreamingMessage { + content: Some(Content::InvocationResponse(res)), + ..Default::default() + }) + .expect("failed to send invocation response"); + + Ok(Async::Ready(())) + } +} + +pub struct Worker; + +impl Worker { + pub fn run(host: &str, port: u16, worker_id: &str, mut registry: Registry<'static>) { + let host_uri: Uri = format!("http://{0}:{1}", host, port).parse().unwrap(); + let (sender, receiver) = unbounded::(); + + // Start by sending a start stream message to the channel + // This will be sent to the host upon connection + sender + .unbounded_send(StreamingMessage { + content: Some(Content::StartStream(StartStream { + worker_id: worker_id.to_owned(), + })), + ..Default::default() + }) + .unwrap(); + + let run = Connect::with_builder( + Connector::new(HttpConnector::new(1)), + HttpBuilder::new().http2_only(true).clone(), + ) + .make_service(Destination::try_from_uri(host_uri.clone()).unwrap()) + .map(move |conn| FunctionRpc::new(HttpOriginService::new(conn, host_uri))) + .map_err(|e| panic!("failed to connect to host: {}", e)) + .and_then(|mut client| { + client + .event_stream(Request::new( + receiver.map_err(|_| panic!("failed to receive from channel")), + )) + .map_err(|e| panic!("failed to start event stream: {}", e)) + }) + .and_then(move |stream| { + stream + .into_inner() + .into_future() + .map_err(|(e, _)| panic!("failed to read worker init request: {}", e)) + .and_then(move |(init_req, stream)| { + Worker::handle_worker_init_request( + sender.clone(), + init_req.expect("expected a worker init request"), + ); + + stream + .for_each(move |req| { + Worker::handle_request(&mut registry, sender.clone(), req); + Ok(()) + }) + .map_err(|e| panic!("fail to read request: {}", e)) + }) + }); + + tokio::run(run); + } + + fn handle_worker_init_request(sender: Sender, req: StreamingMessage) { + match req.content { + Some(Content::WorkerInitRequest(req)) => { + println!( + "Connected to Azure Functions host version {}.", + req.host_version + ); + + // TODO: use the level requested by the Azure functions host + log::set_boxed_logger(Box::new(logger::Logger::new( + log::Level::Info, + sender.clone(), + ))) + .expect("failed to set the global logger instance"); + + set_hook(Box::new(Worker::handle_panic)); + + log::set_max_level(log::LevelFilter::Trace); + + sender + .unbounded_send(StreamingMessage { + content: Some(Content::WorkerInitResponse(WorkerInitResponse { + worker_version: env!("CARGO_PKG_VERSION").to_owned(), + result: Some(StatusResult { + status: Status::Success as i32, + ..Default::default() + }), + ..Default::default() + })), + ..Default::default() + }) + .unwrap(); + } + _ => panic!("expected a worker init request message from the host"), + }; + } + + fn handle_request(registry: &mut Registry<'static>, sender: Sender, req: StreamingMessage) { + match req.content { + Some(Content::FunctionLoadRequest(req)) => { + Worker::handle_function_load_request(registry, sender, req) + } + Some(Content::InvocationRequest(req)) => { + Worker::handle_invocation_request(registry, sender, req) + } + Some(Content::WorkerStatusRequest(req)) => { + Worker::handle_worker_status_request(sender, req) + } + Some(Content::FileChangeEventRequest(_)) => {} + Some(Content::InvocationCancel(_)) => {} + Some(Content::FunctionEnvironmentReloadRequest(_)) => {} + _ => panic!("unexpected message from host: {:?}.", req), + }; + } + + fn handle_function_load_request( + registry: &mut Registry<'static>, + sender: Sender, + req: FunctionLoadRequest, + ) { + let mut result = StatusResult::default(); + + match req.metadata.as_ref() { + Some(metadata) => { + if registry.register(&req.function_id, &metadata.name) { + result.status = Status::Success as i32; + } else { + result.status = Status::Failure as i32; + result.result = format!("Function '{}' does not exist.", metadata.name); + } + } + None => { + result.status = Status::Failure as i32; + result.result = "Function load request metadata is missing.".to_string(); + } + }; + + sender + .unbounded_send(StreamingMessage { + content: Some(Content::FunctionLoadResponse(FunctionLoadResponse { + function_id: req.function_id, + result: Some(result), + ..Default::default() + })), + ..Default::default() + }) + .expect("failed to send function load response"); + } + + fn handle_invocation_request( + registry: &Registry<'static>, + sender: Sender, + req: InvocationRequest, + ) { + if let Some(func) = registry.get(&req.function_id) { + Worker::invoke_function(func, sender, req); + return; + } + + let error = format!("Function with id '{}' does not exist.", req.function_id); + + sender + .unbounded_send(StreamingMessage { + content: Some(Content::InvocationResponse(InvocationResponse { + invocation_id: req.invocation_id, + result: Some(StatusResult { + status: Status::Failure as i32, + result: error, + ..Default::default() + }), + ..Default::default() + })), + ..Default::default() + }) + .expect("failed to send invocation response"); + } + + fn handle_worker_status_request(sender: Sender, _: WorkerStatusRequest) { + sender + .unbounded_send(StreamingMessage { + content: Some(Content::WorkerStatusResponse(WorkerStatusResponse {})), + ..Default::default() + }) + .expect("failed to send worker status response"); + } + + fn invoke_function(func: &'static Function, sender: Sender, req: InvocationRequest) { + match func + .invoker + .as_ref() + .expect("function must have an invoker") + .invoker_fn + { + InvokerFn::Sync(invoker_fn) => { + // `poll_fn` takes FnMut and `blocking` takes FnOnce + // Wrap the request with a RefCell so we can move the request to the invoked function + let id = req.invocation_id.clone(); + let func_id = req.function_id.clone(); + let req = RefCell::new(Some(req)); + + tokio::spawn(ContextFuture::new( + poll_fn(move || { + blocking(|| { + invoker_fn.expect("invoker must have a callback")( + req.replace(None).expect("only a single call to invoker"), + ) + }) + }) + .map_err(|_| ()), + id, + func_id, + &func.name, + sender, + )); + } + InvokerFn::Async(invoker_fn) => { + Worker::invoke_function_async( + invoker_fn.expect("invoker must have a callback"), + func, + sender, + req, + ); + } + }; + } + + #[cfg(feature = "unstable")] + fn invoke_function_async( + invoker_fn: AsyncFn, + func: &'static Function, + sender: Sender, + req: InvocationRequest, + ) { + use futures::future::{FutureExt, TryFutureExt}; + + let id = req.invocation_id.clone(); + let func_id = req.function_id.clone(); + + tokio::spawn(ContextFuture::new( + invoker_fn(req).unit_error().compat(), + id, + func_id, + &func.name, + sender, + )); + } + + #[cfg(not(feature = "unstable"))] + fn invoke_function_async(_: AsyncFn, _: &'static Function, _: Sender, _: InvocationRequest) { + unimplemented!() + } + + fn handle_panic(info: &PanicInfo) { + let backtrace = Backtrace::new(); + match info.location() { + Some(location) => { + error!( + "Azure Function '{}' panicked with '{}', {}:{}:{}{}", + crate::context::CURRENT.with(|c| c.borrow().function_name), + info.payload() + .downcast_ref::<&str>() + .cloned() + .unwrap_or_else(|| info + .payload() + .downcast_ref::() + .map(String::as_str) + .unwrap_or("")), + location.file(), + location.line(), + location.column(), + backtrace + ); + } + None => { + error!( + "Azure Function '{}' panicked with '{}'{}", + crate::context::CURRENT.with(|c| c.borrow().function_name), + info.payload() + .downcast_ref::<&str>() + .cloned() + .unwrap_or_else(|| info + .payload() + .downcast_ref::() + .map(String::as_str) + .unwrap_or("")), + backtrace + ); + } + }; + } +} diff --git a/examples/http/Cargo.toml b/examples/http/Cargo.toml index 0e038af8..ceec67fb 100644 --- a/examples/http/Cargo.toml +++ b/examples/http/Cargo.toml @@ -9,6 +9,7 @@ azure-functions = { path = "../../azure-functions" } log = "0.4.6" serde = { version = "1.0.94", features = ["derive"] } serde_json = "1.0.40" +futures-preview = { version = "0.3.0-alpha.17", optional = true } [features] -unstable = ["azure-functions/unstable"] +unstable = ["azure-functions/unstable", "futures-preview"] diff --git a/examples/http/README.md b/examples/http/README.md index ace9e1db..69b7feed 100644 --- a/examples/http/README.md +++ b/examples/http/README.md @@ -9,13 +9,11 @@ An example HTTP-triggered Azure Function: ```rust use azure_functions::{ bindings::{HttpRequest, HttpResponse}, - func, Context, + func, }; #[func] -pub fn greet(context: Context, req: HttpRequest) -> HttpResponse { - log::info!("Context: {:?}, Request: {:?}", context, req); - +pub fn greet(req: HttpRequest) -> HttpResponse { format!( "Hello from Rust, {}!\n", req.query_params().get("name").map_or("stranger", |x| x) @@ -24,6 +22,27 @@ pub fn greet(context: Context, req: HttpRequest) -> HttpResponse { } ``` +An async version of the `greet` function when the example is built with a nightly compiler and the `unstable` feature enabled: + +```rust +use azure_functions::{ + bindings::{HttpRequest, HttpResponse}, + func, +}; +use futures::future::ready; + +#[func] +pub async fn greet_async(req: HttpRequest) -> HttpResponse { + // Use ready().await to simply demonstrate the async/await feature + ready(format!( + "Hello from Rust, {}!\n", + req.query_params().get("name").map_or("stranger", |x| x) + )) + .await + .into() +} +``` + An example HTTP-triggered Azure Function using JSON for request and response: ```rust @@ -69,6 +88,12 @@ Run the example application with `cargo func run`: $ cargo func run ``` +To run the example with support for async functions when using a nightly compiler: + +```bash +$ cargo func run -- --features unstable +``` + # Invoking the functions ## Invoke the `greet` function @@ -85,6 +110,20 @@ With any luck, you should see the following output: Hello from Rust, Peter! ``` +## Invoke the `greet_async` function + +With support for async functions enabled, invoke the function with `curl`: + +``` +$ curl localhost:8080/api/greet_async\?name=Peter +``` + +With any luck, you should see the following output: + +``` +Hello from Rust, Peter! +``` + ## Invoke the `greet_with_json` function The easiest way to invoke the function is to use `curl`: diff --git a/examples/http/src/functions/greet.rs b/examples/http/src/functions/greet.rs index d9a04447..71d84525 100644 --- a/examples/http/src/functions/greet.rs +++ b/examples/http/src/functions/greet.rs @@ -1,12 +1,10 @@ use azure_functions::{ bindings::{HttpRequest, HttpResponse}, - func, Context, + func, }; #[func] -pub fn greet(context: Context, req: HttpRequest) -> HttpResponse { - log::info!("Context: {:?}, Request: {:?}", context, req); - +pub fn greet(req: HttpRequest) -> HttpResponse { format!( "Hello from Rust, {}!\n", req.query_params().get("name").map_or("stranger", |x| x) diff --git a/examples/http/src/functions/greet_async.rs b/examples/http/src/functions/greet_async.rs new file mode 100644 index 00000000..0def8718 --- /dev/null +++ b/examples/http/src/functions/greet_async.rs @@ -0,0 +1,16 @@ +use azure_functions::{ + bindings::{HttpRequest, HttpResponse}, + func, +}; +use futures::future::ready; + +#[func] +pub async fn greet_async(req: HttpRequest) -> HttpResponse { + // Use ready().await to simply demonstrate the async/await feature + ready(format!( + "Hello from Rust, {}!\n", + req.query_params().get("name").map_or("stranger", |x| x) + )) + .await + .into() +} diff --git a/examples/http/src/functions/mod.rs b/examples/http/src/functions/mod.rs index d470b405..2cda6abd 100644 --- a/examples/http/src/functions/mod.rs +++ b/examples/http/src/functions/mod.rs @@ -2,6 +2,14 @@ // Only the `azure_functions::export!` macro invocation will be preserved. // Export the modules that define Azure Functions here. +#[cfg(feature = "unstable")] +azure_functions::export! { + greet, + greet_async, + greet_with_json, +} + +#[cfg(not(feature = "unstable"))] azure_functions::export! { greet, greet_with_json, diff --git a/examples/http/src/main.rs b/examples/http/src/main.rs index c911cd35..8519d2d3 100644 --- a/examples/http/src/main.rs +++ b/examples/http/src/main.rs @@ -1,3 +1,5 @@ +#![cfg_attr(feature = "unstable", feature(async_await))] + mod functions; pub fn main() {