Skip to content
This repository has been archived by the owner on Jun 2, 2020. It is now read-only.

Commit

Permalink
Implement async Azure Functions.
Browse files Browse the repository at this point in the history
This commit implements support for async Azure Functions.

The feature is behind the `unstable` feature for the azure-functions crate.

Additionally, users must have a dependency on `futures-preview` for the
generated code to build.

Part of this work was refactoring the worker code out of the run command and
into its own file.

The thread-local invocation context also needed to be refactored to better
support a futures-based invocation. This is a breaking change as `Context` is
no longer a binding type; instead, users use `Context::current` to get the
current invocation context.

Closes #270.
  • Loading branch information
Peter Huene committed Jul 8, 2019
1 parent 1b5d1e6 commit d329c54
Show file tree
Hide file tree
Showing 42 changed files with 1,135 additions and 600 deletions.
110 changes: 96 additions & 14 deletions Cargo.lock

Large diffs are not rendered by default.

64 changes: 54 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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`:
Expand All @@ -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.

Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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.

38 changes: 22 additions & 16 deletions azure-functions-codegen/src/func.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -224,19 +223,6 @@ fn bind_input_type(
has_trigger: bool,
binding_args: &mut HashMap<String, (AttributeArgs, Span)>,
) -> 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 {
Expand Down Expand Up @@ -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()),
Expand Down
147 changes: 90 additions & 57 deletions azure-functions-codegen/src/func/invoker.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
}

Expand All @@ -36,7 +50,7 @@ impl<'a> Invoker<'a> {
fn get_input_assignments(&self) -> Vec<TokenStream> {
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;
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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();
Expand All @@ -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;
Expand All @@ -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);
}
}
}
Loading

0 comments on commit d329c54

Please sign in to comment.