3
3
4
4
import { format } from 'util' ;
5
5
import { AzureFunctionsRpcMessages as rpc } from '../../azure-functions-language-worker-protobuf/src/rpc' ;
6
- import { CreateContextAndInputs , LogCallback , ResultCallback } from '../Context' ;
6
+ import { CreateContextAndInputs } from '../Context' ;
7
7
import { toTypedData } from '../converters' ;
8
+ import { isError } from '../utils/ensureErrorType' ;
8
9
import { toRpcStatus } from '../utils/toRpcStatus' ;
9
10
import { WorkerChannel } from '../WorkerChannel' ;
10
11
import LogCategory = rpc . RpcLog . RpcLogCategory ;
@@ -15,28 +16,89 @@ import LogLevel = rpc.RpcLog.Level;
15
16
* @param requestId gRPC message request id
16
17
* @param msg gRPC message content
17
18
*/
18
- export function invocationRequest ( channel : WorkerChannel , requestId : string , msg : rpc . InvocationRequest ) {
19
+ export async function invocationRequest ( channel : WorkerChannel , requestId : string , msg : rpc . InvocationRequest ) {
20
+ const response : rpc . IInvocationResponse = {
21
+ invocationId : msg . invocationId ,
22
+ result : toRpcStatus ( ) ,
23
+ } ;
24
+ // explicitly set outputData to empty array to concat later
25
+ response . outputData = [ ] ;
26
+
27
+ let isDone = false ;
28
+ let resultIsPromise = false ;
29
+
19
30
const info = channel . functionLoader . getInfo ( msg . functionId ) ;
20
- const logCallback : LogCallback = ( level , category , ...args ) => {
31
+
32
+ function log ( level : LogLevel , category : LogCategory , ...args : any [ ] ) {
21
33
channel . log ( {
22
34
invocationId : msg . invocationId ,
23
35
category : `${ info . name } .Invocation` ,
24
36
message : format . apply ( null , < [ any , any [ ] ] > args ) ,
25
37
level : level ,
26
38
logCategory : category ,
27
39
} ) ;
28
- } ;
40
+ }
41
+ function systemLog ( level : LogLevel , ...args : any [ ] ) {
42
+ log ( level , LogCategory . System , ...args ) ;
43
+ }
44
+ function userLog ( level : LogLevel , ...args : any [ ] ) {
45
+ if ( isDone ) {
46
+ let badAsyncMsg =
47
+ "Warning: Unexpected call to 'log' on the context object after function execution has completed. Please check for asynchronous calls that are not awaited or calls to 'done' made before function execution completes. " ;
48
+ badAsyncMsg += `Function name: ${ info . name } . Invocation Id: ${ msg . invocationId } . ` ;
49
+ badAsyncMsg += `Learn more: https://go.microsoft.com/fwlink/?linkid=2097909 ` ;
50
+ systemLog ( LogLevel . Warning , badAsyncMsg ) ;
51
+ }
52
+ log ( level , LogCategory . User , ...args ) ;
53
+ }
29
54
30
55
// Log invocation details to ensure the invocation received by node worker
31
- logCallback ( LogLevel . Debug , LogCategory . System , 'Received FunctionInvocationRequest' ) ;
56
+ systemLog ( LogLevel . Debug , 'Received FunctionInvocationRequest' ) ;
32
57
33
- const resultCallback : ResultCallback = ( err : unknown , result ) => {
34
- const response : rpc . IInvocationResponse = {
35
- invocationId : msg . invocationId ,
36
- result : toRpcStatus ( err ) ,
37
- } ;
38
- // explicitly set outputData to empty array to concat later
39
- response . outputData = [ ] ;
58
+ function onDone ( ) : void {
59
+ if ( isDone ) {
60
+ const message = resultIsPromise
61
+ ? "Error: Choose either to return a promise or call 'done'. Do not use both in your script."
62
+ : "Error: 'done' has already been called. Please check your script for extraneous calls to 'done'." ;
63
+ systemLog ( LogLevel . Error , message ) ;
64
+ }
65
+ isDone = true ;
66
+ }
67
+
68
+ const { context, inputs, doneEmitter } = CreateContextAndInputs ( info , msg , userLog ) ;
69
+ try {
70
+ const legacyDoneTask = new Promise ( ( resolve , reject ) => {
71
+ doneEmitter . on ( 'done' , ( err ?: unknown , result ?: any ) => {
72
+ onDone ( ) ;
73
+ if ( isError ( err ) ) {
74
+ reject ( err ) ;
75
+ } else {
76
+ resolve ( result ) ;
77
+ }
78
+ } ) ;
79
+ } ) ;
80
+
81
+ let userFunction = channel . functionLoader . getFunc ( msg . functionId ) ;
82
+ userFunction = channel . runInvocationRequestBefore ( context , userFunction ) ;
83
+ let rawResult = userFunction ( context , ...inputs ) ;
84
+ resultIsPromise = rawResult && typeof rawResult . then === 'function' ;
85
+ let resultTask : Promise < any > ;
86
+ if ( resultIsPromise ) {
87
+ rawResult = Promise . resolve ( rawResult ) . then ( ( r ) => {
88
+ onDone ( ) ;
89
+ return r ;
90
+ } ) ;
91
+ resultTask = Promise . race ( [ rawResult , legacyDoneTask ] ) ;
92
+ } else {
93
+ resultTask = legacyDoneTask ;
94
+ }
95
+
96
+ const result = await resultTask ;
97
+
98
+ // Allow HTTP response from context.res if HTTP response is not defined from the context.bindings object
99
+ if ( info . httpOutputName && context . res && context . bindings [ info . httpOutputName ] === undefined ) {
100
+ context . bindings [ info . httpOutputName ] = context . res ;
101
+ }
40
102
41
103
// As legacy behavior, falsy values get serialized to `null` in AzFunctions.
42
104
// This breaks Durable Functions expectations, where customers expect any
@@ -45,86 +107,61 @@ export function invocationRequest(channel: WorkerChannel, requestId: string, msg
45
107
// values get serialized.
46
108
const isDurableBinding = info ?. bindings ?. name ?. type == 'activityTrigger' ;
47
109
48
- try {
49
- if ( result || ( isDurableBinding && result != null ) ) {
50
- const returnBinding = info . getReturnBinding ( ) ;
51
- // Set results from return / context.done
52
- if ( result . return || ( isDurableBinding && result . return != null ) ) {
53
- // $return binding is found: return result data to $return binding
54
- if ( returnBinding ) {
55
- response . returnValue = returnBinding . converter ( result . return ) ;
56
- // $return binding is not found: read result as object of outputs
57
- } else {
58
- response . outputData = Object . keys ( info . outputBindings )
59
- . filter ( ( key ) => result . return [ key ] !== undefined )
60
- . map (
61
- ( key ) =>
62
- < rpc . IParameterBinding > {
63
- name : key ,
64
- data : info . outputBindings [ key ] . converter ( result . return [ key ] ) ,
65
- }
66
- ) ;
67
- }
68
- // returned value does not match any output bindings (named or $return)
69
- // if not http, pass along value
70
- if ( ! response . returnValue && response . outputData . length == 0 && ! info . hasHttpTrigger ) {
71
- response . returnValue = toTypedData ( result . return ) ;
72
- }
73
- }
74
- // Set results from context.bindings
75
- if ( result . bindings ) {
76
- response . outputData = response . outputData . concat (
77
- Object . keys ( info . outputBindings )
78
- // Data from return prioritized over data from context.bindings
79
- . filter ( ( key ) => {
80
- const definedInBindings : boolean = result . bindings [ key ] !== undefined ;
81
- const hasReturnValue = ! ! result . return ;
82
- const hasReturnBinding = ! ! returnBinding ;
83
- const definedInReturn : boolean =
84
- hasReturnValue && ! hasReturnBinding && result . return [ key ] !== undefined ;
85
- return definedInBindings && ! definedInReturn ;
86
- } )
87
- . map (
88
- ( key ) =>
89
- < rpc . IParameterBinding > {
90
- name : key ,
91
- data : info . outputBindings [ key ] . converter ( result . bindings [ key ] ) ,
92
- }
93
- )
110
+ const returnBinding = info . getReturnBinding ( ) ;
111
+ // Set results from return / context.done
112
+ if ( result || ( isDurableBinding && result != null ) ) {
113
+ // $return binding is found: return result data to $return binding
114
+ if ( returnBinding ) {
115
+ response . returnValue = returnBinding . converter ( result ) ;
116
+ // $return binding is not found: read result as object of outputs
117
+ } else {
118
+ response . outputData = Object . keys ( info . outputBindings )
119
+ . filter ( ( key ) => result [ key ] !== undefined )
120
+ . map (
121
+ ( key ) =>
122
+ < rpc . IParameterBinding > {
123
+ name : key ,
124
+ data : info . outputBindings [ key ] . converter ( result [ key ] ) ,
125
+ }
94
126
) ;
95
- }
96
127
}
97
- } catch ( err ) {
98
- response . result = toRpcStatus ( err ) ;
128
+ // returned value does not match any output bindings (named or $return)
129
+ // if not http, pass along value
130
+ if ( ! response . returnValue && response . outputData . length == 0 && ! info . hasHttpTrigger ) {
131
+ response . returnValue = toTypedData ( result ) ;
132
+ }
99
133
}
100
- channel . eventStream . write ( {
101
- requestId : requestId ,
102
- invocationResponse : response ,
103
- } ) ;
104
-
105
- channel . runInvocationRequestAfter ( context ) ;
106
- } ;
107
-
108
- const { context, inputs } = CreateContextAndInputs ( info , msg , logCallback , resultCallback ) ;
109
- let userFunction = channel . functionLoader . getFunc ( msg . functionId ) ;
110
-
111
- userFunction = channel . runInvocationRequestBefore ( context , userFunction ) ;
112
-
113
- // catch user errors from the same async context in the event loop and correlate with invocation
114
- // throws from asynchronous work (setTimeout, etc) are caught by 'unhandledException' and cannot be correlated with invocation
115
- try {
116
- const result = userFunction ( context , ...inputs ) ;
117
-
118
- if ( result && typeof result . then === 'function' ) {
119
- result
120
- . then ( ( result ) => {
121
- ( < any > context . done ) ( null , result , true ) ;
122
- } )
123
- . catch ( ( err ) => {
124
- ( < any > context . done ) ( err , null , true ) ;
125
- } ) ;
134
+ // Set results from context.bindings
135
+ if ( context . bindings ) {
136
+ response . outputData = response . outputData . concat (
137
+ Object . keys ( info . outputBindings )
138
+ // Data from return prioritized over data from context.bindings
139
+ . filter ( ( key ) => {
140
+ const definedInBindings : boolean = context . bindings [ key ] !== undefined ;
141
+ const hasReturnValue = ! ! result ;
142
+ const hasReturnBinding = ! ! returnBinding ;
143
+ const definedInReturn : boolean =
144
+ hasReturnValue && ! hasReturnBinding && result [ key ] !== undefined ;
145
+ return definedInBindings && ! definedInReturn ;
146
+ } )
147
+ . map (
148
+ ( key ) =>
149
+ < rpc . IParameterBinding > {
150
+ name : key ,
151
+ data : info . outputBindings [ key ] . converter ( context . bindings [ key ] ) ,
152
+ }
153
+ )
154
+ ) ;
126
155
}
127
156
} catch ( err ) {
128
- resultCallback ( err ) ;
157
+ response . result = toRpcStatus ( err ) ;
158
+ isDone = true ;
129
159
}
160
+
161
+ channel . eventStream . write ( {
162
+ requestId : requestId ,
163
+ invocationResponse : response ,
164
+ } ) ;
165
+
166
+ channel . runInvocationRequestAfter ( context ) ;
130
167
}
0 commit comments