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

Commit d329c54

Browse files
author
Peter Huene
committed
Implement async Azure Functions.
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.
1 parent 1b5d1e6 commit d329c54

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1135
-600
lines changed

Cargo.lock

Lines changed: 96 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,44 @@ in [Rust](https://www.rust-lang.org/).
2121
A simple HTTP-triggered Azure Function:
2222

2323
```rust
24-
use azure_functions::bindings::{HttpRequest, HttpResponse};
25-
use azure_functions::func;
24+
use azure_functions::{
25+
bindings::{HttpRequest, HttpResponse},
26+
func,
27+
};
2628

2729
#[func]
2830
pub fn greet(req: HttpRequest) -> HttpResponse {
29-
// Log the message with the Azure Functions Host
30-
info!("Request: {:?}", req);
31-
3231
format!(
3332
"Hello from Rust, {}!\n",
3433
req.query_params().get("name").map_or("stranger", |x| x)
35-
).into()
34+
)
35+
.into()
36+
}
37+
```
38+
39+
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:
40+
41+
```rust
42+
use azure_functions::{
43+
bindings::{HttpRequest, HttpResponse},
44+
func,
45+
};
46+
use futures::future::ready;
47+
48+
#[func]
49+
pub async fn greet_async(req: HttpRequest) -> HttpResponse {
50+
// Use ready().await to simply demonstrate the async/await feature
51+
ready(format!(
52+
"Hello from Rust, {}!\n",
53+
req.query_params().get("name").map_or("stranger", |x| x)
54+
))
55+
.await
56+
.into()
3657
}
3758
```
3859

60+
See [Building an async Azure Functions application](#building-an-async-azure-functions-application) for more information.
61+
3962
## Get Started
4063

4164
- [More Examples](https://github.com/peterhuene/azure-functions-rs/tree/master/examples)
@@ -54,6 +77,7 @@ pub fn greet(req: HttpRequest) -> HttpResponse {
5477
- [Creating a new Azure Functions application](#creating-a-new-azure-functions-application)
5578
- [Adding a simple HTTP-triggered application](#adding-a-simple-http-triggered-application)
5679
- [Building the Azure Functions application](#building-the-azure-functions-application)
80+
- [Building an async Azure Functions application](#building-an-async-azure-functions-application)
5781
- [Running the Azure Functions application](#running-the-azure-functions-application)
5882
- [Debugging the Azure Functions application](#debugging-the-azure-functions-application)
5983
- [Deploying the Azure Functions application](#deploying-the-azure-functions-application)
@@ -138,6 +162,24 @@ cargo build --features unstable
138162

139163
This enables Azure Functions for Rust to emit diagnostic messages that will include the position of an error within an attribute.
140164

165+
## Building an async Azure Functions application
166+
167+
To build with support for async Azure Functions, add the following to your `Cargo.toml`:
168+
169+
```toml
170+
[dependencies]
171+
futures-preview = { version = "0.3.0-alpha.17", optional = true }
172+
173+
[features]
174+
unstable = ["azure-functions/unstable", "futures-preview"]
175+
```
176+
177+
And then build with the `unstable` feature:
178+
179+
```bash
180+
cargo build --features unstable
181+
```
182+
141183
## Running the Azure Functions application
142184

143185
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`:
146188
cargo func run
147189
```
148190

191+
If you need to enable the `unstable` feature, pass the `--features` option to cargo:
192+
193+
```bash
194+
cargo func run -- --features unstable
195+
```
196+
149197
The `cargo func run` command builds and runs your application locally using the Azure Function Host that was
150198
installed by the Azure Functions Core Tools.
151199

@@ -228,9 +276,6 @@ The current list of supported bindings:
228276
| [Table](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.Table.html) | Input and Ouput Table | in, out | No |
229277
| [TimerInfo](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.TimerInfo.html) | Timer Trigger | in | No |
230278
| [TwilioSmsMessage](https://docs.rs/azure-functions/latest/azure_functions/bindings/struct.TwilioSmsMessage.html) | Twilio SMS Message Output | out | Yes | Yes |
231-
| [Context](https://docs.rs/azure-functions/latest/azure_functions/struct.Context.html)* | Invocation Context | N/A | N/A |
232-
233-
\****Note: the `Context` binding is not an Azure Functions binding; it is used to pass information about the function being invoked.***
234279

235280
More bindings will be implemented in the future, including support for retreiving data from custom bindings.
236281

@@ -358,4 +403,3 @@ pub fn example(...) -> ((), Blob) {
358403
```
359404

360405
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.
361-

azure-functions-codegen/src/func.rs

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use azure_functions_shared::codegen::{
77
Binding, BindingFactory, INPUT_BINDINGS, INPUT_OUTPUT_BINDINGS, OUTPUT_BINDINGS, TRIGGERS,
88
VEC_INPUT_BINDINGS, VEC_OUTPUT_BINDINGS,
99
},
10-
get_string_value, iter_attribute_args, last_segment_in_path, macro_panic, Function,
10+
get_string_value, iter_attribute_args, last_segment_in_path, macro_panic, Function, InvokerFn,
1111
};
1212
use invoker::Invoker;
1313
use output_bindings::OutputBindings;
@@ -23,7 +23,6 @@ use syn::{
2323

2424
pub const OUTPUT_BINDING_PREFIX: &str = "output";
2525
const RETURN_BINDING_NAME: &str = "$return";
26-
const CONTEXT_TYPE_NAME: &str = "Context";
2726

2827
fn validate_function(func: &ItemFn) {
2928
match func.vis {
@@ -224,19 +223,6 @@ fn bind_input_type(
224223
has_trigger: bool,
225224
binding_args: &mut HashMap<String, (AttributeArgs, Span)>,
226225
) -> Binding {
227-
let last_segment = last_segment_in_path(&tp.path);
228-
let type_name = last_segment.ident.to_string();
229-
230-
if type_name == CONTEXT_TYPE_NAME {
231-
if let Some(m) = mutability {
232-
macro_panic(
233-
m.span(),
234-
"context bindings cannot be passed by mutable reference",
235-
);
236-
}
237-
return Binding::Context;
238-
}
239-
240226
let factory = get_input_binding_factory(tp, mutability, has_trigger);
241227

242228
match pattern {
@@ -498,7 +484,27 @@ pub fn func_impl(
498484
func.name = Cow::Owned(target_name.clone());
499485
}
500486

501-
func.invoker_name = Some(Cow::Owned(invoker.name()));
487+
match target.asyncness {
488+
Some(asyncness) => {
489+
if cfg!(feature = "unstable") {
490+
func.invoker = Some(azure_functions_shared::codegen::Invoker {
491+
name: Cow::Owned(invoker.name()),
492+
invoker_fn: InvokerFn::Async(None),
493+
});
494+
} else {
495+
macro_panic(
496+
asyncness.span(),
497+
"async Azure Functions require a nightly compiler with the 'unstable' feature enabled",
498+
);
499+
}
500+
}
501+
None => {
502+
func.invoker = Some(azure_functions_shared::codegen::Invoker {
503+
name: Cow::Owned(invoker.name()),
504+
invoker_fn: InvokerFn::Sync(None),
505+
});
506+
}
507+
}
502508

503509
let const_name = Ident::new(
504510
&format!("__{}_FUNCTION", target_name.to_uppercase()),

azure-functions-codegen/src/func/invoker.rs

Lines changed: 90 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::func::{get_generic_argument_type, OutputBindings, CONTEXT_TYPE_NAME};
1+
use crate::func::{get_generic_argument_type, OutputBindings};
22
use azure_functions_shared::codegen::{bindings::TRIGGERS, last_segment_in_path};
33
use azure_functions_shared::util::to_camel_case;
44
use proc_macro2::TokenStream;
@@ -21,10 +21,24 @@ impl<'a> Invoker<'a> {
2121
}
2222
}
2323

24+
fn is_trigger_type(ty: &Type) -> bool {
25+
match Invoker::deref_arg_type(ty) {
26+
Type::Path(tp) => {
27+
TRIGGERS.contains_key(last_segment_in_path(&tp.path).ident.to_string().as_str())
28+
}
29+
Type::Paren(tp) => Invoker::is_trigger_type(&tp.elem),
30+
_ => false,
31+
}
32+
}
33+
}
34+
35+
struct CommonInvokerTokens<'a>(pub &'a ItemFn);
36+
37+
impl<'a> CommonInvokerTokens<'a> {
2438
fn get_input_args(&self) -> (Vec<&'a Ident>, Vec<&'a Type>) {
2539
self.iter_args()
2640
.filter_map(|(name, arg_type)| {
27-
if Invoker::is_context_type(arg_type) | Invoker::is_trigger_type(arg_type) {
41+
if Invoker::is_trigger_type(arg_type) {
2842
return None;
2943
}
3044

@@ -36,7 +50,7 @@ impl<'a> Invoker<'a> {
3650
fn get_input_assignments(&self) -> Vec<TokenStream> {
3751
self.iter_args()
3852
.filter_map(|(_, arg_type)| {
39-
if Invoker::is_context_type(arg_type) | Invoker::is_trigger_type(arg_type) {
53+
if Invoker::is_trigger_type(arg_type) {
4054
return None;
4155
}
4256

@@ -66,13 +80,6 @@ impl<'a> Invoker<'a> {
6680
fn get_args_for_call(&self) -> Vec<::proc_macro2::TokenStream> {
6781
self.iter_args()
6882
.map(|(name, arg_type)| {
69-
if Invoker::is_context_type(arg_type) {
70-
if let Type::Reference(_) = arg_type {
71-
return quote!(&__ctx)
72-
}
73-
return quote!(__ctx.clone());
74-
}
75-
7683
let name_str = name.to_string();
7784

7885
if let Type::Reference(tr) = arg_type {
@@ -99,32 +106,10 @@ impl<'a> Invoker<'a> {
99106
_ => panic!("expected captured arguments"),
100107
})
101108
}
102-
103-
fn is_context_type(ty: &Type) -> bool {
104-
match Invoker::deref_arg_type(ty) {
105-
Type::Path(tp) => last_segment_in_path(&tp.path).ident == CONTEXT_TYPE_NAME,
106-
Type::Paren(tp) => Invoker::is_context_type(&tp.elem),
107-
_ => false,
108-
}
109-
}
110-
111-
fn is_trigger_type(ty: &Type) -> bool {
112-
match Invoker::deref_arg_type(ty) {
113-
Type::Path(tp) => {
114-
TRIGGERS.contains_key(last_segment_in_path(&tp.path).ident.to_string().as_str())
115-
}
116-
Type::Paren(tp) => Invoker::is_trigger_type(&tp.elem),
117-
_ => false,
118-
}
119-
}
120109
}
121110

122-
impl ToTokens for Invoker<'_> {
111+
impl ToTokens for CommonInvokerTokens<'_> {
123112
fn to_tokens(&self, tokens: &mut ::proc_macro2::TokenStream) {
124-
let invoker = Ident::new(
125-
&format!("{}{}", INVOKER_PREFIX, self.0.ident.to_string()),
126-
self.0.ident.span(),
127-
);
128113
let target = &self.0.ident;
129114

130115
let (args, types) = self.get_input_args();
@@ -139,13 +124,7 @@ impl ToTokens for Invoker<'_> {
139124

140125
let args_for_call = self.get_args_for_call();
141126

142-
let output_bindings = OutputBindings(self.0);
143-
144-
quote!(#[allow(dead_code)]
145-
fn #invoker(
146-
__name: &str,
147-
__req: ::azure_functions::rpc::InvocationRequest,
148-
) -> ::azure_functions::rpc::InvocationResponse {
127+
quote!(
149128
use azure_functions::{IntoVec, FromVec};
150129

151130
let mut #trigger_arg: Option<#trigger_type> = None;
@@ -155,33 +134,87 @@ impl ToTokens for Invoker<'_> {
155134

156135
for __param in __req.input_data.into_iter() {
157136
match __param.name.as_str() {
158-
#trigger_name => #trigger_arg = Some(
159-
#trigger_type::new(
160-
__param.data.expect("expected parameter binding data"),
161-
__metadata.take().expect("expected only one trigger")
137+
#trigger_name => #trigger_arg = Some(
138+
#trigger_type::new(
139+
__param.data.expect("expected parameter binding data"),
140+
__metadata.take().expect("expected only one trigger")
162141
)
163142
),
164-
#(#arg_names => #args_for_match = Some(#arg_assignments),)*
143+
#(#arg_names => #args_for_match = Some(#arg_assignments),)*
165144
_ => panic!(format!("unexpected parameter binding '{}'", __param.name)),
166145
};
167146
}
168147

169-
let __ctx = ::azure_functions::Context::new(&__req.invocation_id, &__req.function_id, __name);
170148
let __ret = #target(#(#args_for_call,)*);
149+
)
150+
.to_tokens(tokens);
151+
}
152+
}
171153

172-
let mut __res = ::azure_functions::rpc::InvocationResponse {
173-
invocation_id: __req.invocation_id,
174-
result: Some(::azure_functions::rpc::StatusResult {
175-
status: ::azure_functions::rpc::status_result::Status::Success as i32,
176-
..Default::default()
177-
}),
178-
..Default::default()
179-
};
154+
impl ToTokens for Invoker<'_> {
155+
fn to_tokens(&self, tokens: &mut ::proc_macro2::TokenStream) {
156+
let ident = Ident::new(
157+
&format!("{}{}", INVOKER_PREFIX, self.0.ident.to_string()),
158+
self.0.ident.span(),
159+
);
160+
161+
let common_tokens = CommonInvokerTokens(&self.0);
180162

181-
#output_bindings
163+
let output_bindings = OutputBindings(self.0);
164+
165+
if self.0.asyncness.is_some() {
166+
quote!(
167+
#[allow(dead_code)]
168+
fn #ident(
169+
__req: ::azure_functions::rpc::InvocationRequest,
170+
) -> ::azure_functions::codegen::InvocationFuture {
171+
#common_tokens
172+
173+
use futures::future::FutureExt;
174+
175+
let __id = __req.invocation_id;
176+
177+
Box::pin(
178+
__ret.then(move |__ret| {
179+
let mut __res = ::azure_functions::rpc::InvocationResponse {
180+
invocation_id: __id,
181+
result: Some(::azure_functions::rpc::StatusResult {
182+
status: ::azure_functions::rpc::status_result::Status::Success as i32,
183+
..Default::default()
184+
}),
185+
..Default::default()
186+
};
187+
188+
#output_bindings
189+
190+
::futures::future::ready(__res)
191+
})
192+
)
193+
}
194+
).to_tokens(tokens);
195+
} else {
196+
quote!(
197+
#[allow(dead_code)]
198+
fn #ident(
199+
__req: ::azure_functions::rpc::InvocationRequest,
200+
) -> ::azure_functions::rpc::InvocationResponse {
201+
#common_tokens
202+
203+
let mut __res = ::azure_functions::rpc::InvocationResponse {
204+
invocation_id: __req.invocation_id,
205+
result: Some(::azure_functions::rpc::StatusResult {
206+
status: ::azure_functions::rpc::status_result::Status::Success as i32,
207+
..Default::default()
208+
}),
209+
..Default::default()
210+
};
182211

183-
__res
212+
#output_bindings
184213

185-
}).to_tokens(tokens);
214+
__res
215+
}
216+
)
217+
.to_tokens(tokens);
218+
}
186219
}
187220
}

0 commit comments

Comments
 (0)