OCI allows for cloud native application development, using facilities such as serverless functions, a light weight API gateway, a streaming service for asynchronous communication based on event messaging and built-in monitoring. In previous articles, I have gone over several aspects of serverless functions and API Gateways to create and expose functions. One aspect we have not really touched upon until is the interaction between functions. How does one function trigger execution of another function. Time to focus on this all too common requirement.
This picture visualizes the most common ways of functions invoking other functions.
- Functions make a direct synchronous call using the OCI CLI or SDK or at least a signed REST API request
- Functions can make a simple REST API call to an internal or external API Gateway that routes the synchronous request to a function backend
- Functions way want to trigger execution of another function in an asynchronous way – fire and forget style; in this case they could publish an event on a Stream and have decoupled listener consume the message and trigger the target function
- EDIT (26th February 2020): OCI Notification Topics now support subscription of an OCI Function. This means that you can publish an event to an Notification Topic (for example from a function) and have a function be triggered by that event. This in fact provide an asynchronous function to function call. Note: at present, the number messages published from a Notification Topic is limited at 50 per minute.
There is actually yet another way to trigger execution of a function – although in our current discussion, it feels quite contrived. OCI Events – published when things happen in Oracle Cloud Infrastructure, for example when Cloud resources are created, manipulated or deleted – can be used to trigger a Function. The best example of an event that could be used meaningfully to trigger a function is the event emitted by Object Storage whenever a file is created [or updated or removed].
Let’s discuss the second and third option – Function calls function through API Gateway (synchronously) and function calls function through Streaming (and listener) (asynchronously). EDIT (26 February 2020): checkout this article for an example of an aysnchronous call from function to function using a Notification Topic as an in between queue mechanism: https://www.ateam-oracle.com/oci-increases-notifications-fan-out-by-adding-functions-support .
Note: the sources discussed in this article are on GitHub: https://github.com/lucasjellema/oci-cloud-native-explorations/tree/master/rss-feeder and https://github.com/lucasjellema/oci-cloud-native-explorations/tree/master/function-trigger-listener.
Function calls function through API Gateway (synchronously)
This is nothing very special to be honest. A function – written in any language you like – makes an HTTP(S) call to an endpoint, including headers, URL path and query parameters and a body. It does not (need to) know that behind the endpoint is hiding a fellow function – that could be implemented in a completely different language. For tracing sake, the function could make use of a combined ‘conversation identifier’, for example the opc-request-id defined in the request to the initial function.
I will show a very simple example of a function that reads an RSS feed from a URL passed as parameter and has that feed’s contents written as a JSON document to OCI Object Storage by a second function: file-writer, introduced in a recent article.
The salient section of the code where Function rss-feeder calls Function file-writer is shown here:
The call made here is a simple HTTP POST call to a REST endpoint. Nothing to indicate that there is a special relation between the two functions. However, this call is synchronous – which means there is a direct runtime dependency from rss-feeder on file-writer. A dependency that does not have to be synchronous and therefore should not be synchronous. The next section discusses how we can turn the trigger from rss-feeder to file-writer into an asynchronous one.
A few screenshots showing the process of deploying, configuring and invoking the function from the Fn development environment:
Deploy:
Configure
Invoke
With the following result:
When the function has been deployed, I can create a new route on the API Gateway to make the function accessible from external clients such as Postman.
When that is done, I can invoke my function (or sequence of functions) as is shown below:
With the following result on Object Storage:
And the expected file contents:
Function calls function through Streaming (asynchronously)
One function may want to trigger execution of another one in an asynchronous way – without any runtime dependency. In fact, this should be the preferred model for function interaction. Synchronous interaction has to be motivated.
However, how do we implement this asynchronous interaction? How can we make one function kick off a second without a direct call?
A common way is through a neutral intermediary. An event handling mechanism of some sort. On Oracle Cloud Infrastructure, at this moment our best option (with the imminent demise of Oracle Messaging Cloud Service) is use of the Streaming service – an Apache Kafka-like durable event platform.
The solution architecture for the example from the previous section would look like this in an asynchronous approach.
The main differences: Function rss-feeder publishes an event to an Oracle Streaming stream instead of invoking and endpoint on API Gateway and there is a new component – the listener – that consumes the event published by rss-feeder and calls the file-writer endpoint on API Gateway. Nothing needs to change for function file-writer or for the configuration of the API Gateway.
Before we can implement the event publisher and consumer, we need to create the stream itself.
Next, function rss-feeder is modified; instead of calling the /persist endpoint on API Gateway, it now publishes an event to the function-triggering-stream.
A new application is created – in my case called function-trigger-listener. It listens for messages on the function-triggering-stream and if it consumes one or more, it will try to act on them. If a message specifies file-writer as its key, the application knows what to do: it unpacks the payload (base64 and URI encoded) and invokes the file-writer function.
The salient code section of function-trigger-listener:
The rss-feeder function fetched the RSS feed content and published the event to the Stream. Then it was done. The function-trigger-listener consumes the event that contains the file name and the content of the RSS feed and then invokes the API Gateway endpoint of the file-writer function, that creates the file on Object Storage. There is no dependency – not logically nor run-time between rss-feeder and file-writer. The latency and throughput of the rss-feeder are improved and the robustness and scalability of the implementation is increased.
An example:
run the RSS Feeder
An event is published:
At some point later in time, run the function-trigger-listener:
Then check the Object Storage Cloud’s fn-bucket for the file that should be created under the name amis-blog-rss-extra-edition.json that was passed to the RSS Feeder.
Resources
Sources discussed in this article on GitHub: https://github.com/lucasjellema/oci-cloud-native-explorations/tree/master/rss-feeder and https://github.com/lucasjellema/oci-cloud-native-explorations/tree/master/function-trigger-listener.
NPM module for converting RSS feeds (XML format) to JSON – https://www.npmjs.com/package/rss-to-json
RSS Feed for AMIS Technology Blog: https://technology.amis.nl/feed/
How to convert Callback Function construction to Promise syntax that fits in with await and async in JavaScript: https://medium.com/javascript-in-plain-english/converting-javascript-callbacks-to-promise-and-async-await-replacing-async-waterfall-method-with-3c8b7487e0b9
Why introduce Streaming into this when as you have already said you can setup events to fire when objects are created in a bucket? In this case you have a generic function that does nothing but persist the payload and other metadata about the invocation to object store. You setup a rule to match the data and trigger a downstream call to the functions that no what to do with the data?
Alternatively you could use the OCI SDK and the notifications service rather then using events in OCI. A subscription in this case would invoke a function via generic HTTPS and depending on how much data is being processed you either chuck the data in the payload (max 64KB) or just put in some id to the object in object store?
Hi Jim,
Thanks for your reaction. I agree with you that streaming is not the ideal solution. However, creating a ‘fake’ object in order to trigger an event (indirectly) and use that as a means to asynchronously invoke a function is not great either. I would prefer to have API Gateway support a ‘fire and forget’ style of call, that would do the back end call asynchronously with regard to the caller. And I would like to have a simple (RabbitMQ style?) queuing mechanism to which Functions can subscribe. I understand Notifications will soon be able to subscribe Functions – which would be nice. Hopefully the limitation on the number of notifications per minute is then raised too.
This article is also a way of demonstrating how Streams can be published to and consumed from.
Thanks,
Lucas