Resilience of a cloud application is its ability to maintain its functionality, availability, and performance even when failures, disruptions or unexpected events occur. Resilience strategies in cloud applications involve implementing design patterns, architectural practices, and technologies that ensure the system can gracefully handle failures and recover from them without causing significant disruptions to users.
In this blog post, I’ll discuss a resilience pattern called timeouts within the context of developing applications on BTP using the Cloud Application Programming Model (CAP).
Timeouts are similar to alarms in computer programs. When a task, such as running an API, reading a file, or executing a database query, takes too long, the program stops waiting. This prevents operations from becoming stuck, which could otherwise inefficiently use server resources. Using timeouts strategically is essential to ensure that applications are more resistant to external attacks caused by resource exhaustion, such as Denial of Service (DoS) attacks, and Event Handler Poisoning attacks.
Resilience patterns or strategies like timeouts can be applied to both inbound and outbound requests of a CAP application.
CDS Middlewares:
CAP application framework leverages Express.js to serve services over a specific protocol. To enhance flexibility and modularity, the framework utilizes a middleware architecture. Middlewares are functions that can intercept and modify incoming requests or outgoing responses.
For each service served at a certain protocol, the framework registers a configurable set of express middlewares like context, trace, auth, ctx_auth, ctx_model etc. More information is available at this page: cds.middlewares
CDS Plugins:
The concept of CDS plugins allows developers keep specific functions separate from the main application code. These plugins can be used in multiple applications promoting modularity and code reuse. With plugins, capabilities of CAP application cab be enhanced by connecting to standard events and adding event handlers. If you want to get your hands dirty with plugin concept, refer following resources:
Let’s look at different options to apply timeout resilience for incoming requests to CAP application.
→ Via Standalone Approuter
Should a response fail to return within the configured timeframe, an error message – ‘Error: socket hang up’ – will be generated.
If response is not returned within the configured timeframe, an error message – ‘504 Gateway Timeout’ – will be generated.
→ Via CDS Plugin
// Service Details: service.cds
service MyService {
function getData(wait: Boolean) returns array of String(20);
}
// Service Logic: service.js
const cds = require("@sap/cds");
const { setTimeout } = require("timers/promises");
module.exports = cds.service.impl(async function (srv) {
srv.on("getData", async (req) => {
if (req.data.wait) {
console.log('[DB] reading data from db - started!');
await setTimeout(5 * 1000);
console.log('[DB] reading data from db - finished!');
}
if (req.data.wait) {
console.log('[API] reading data from api - started!');
await setTimeout(5 * 1000);
console.log('[API] reading data from api - finished!');
}
if (req.data.wait) {
console.log('processing both sets of data started!');
await setTimeout(5 * 1000);
console.log('processing both sets of data finished!');
}
return ['StringData1', 'StringData2'];
})
})
Service MyService has a function called getData which contains following logic:
All of the above steps add 5 seconds if wait parameter is set to true. This is done to test our application. Note that, we are simulating 5 seconds using setTimeout function. The setTimeout function from the timers/promises module in Node.js allows you to utilize the setTimeout function as a promise, enabling asynchronous operations with a more convenient syntax.
In summary, we have a function in MyService which takes around 15 second to process and return some data.
"workspaces":["resilience-plugin"]
It is possible to release these plugins as npm package and use it in multiple applications.
Additionally, plugin must contain cds-plugin.js file. This file contains the logic for your plugin and integrates it into your CAP (Core Data and Services) application. Let’s look at our current example:
const cds = require("@sap/cds");
const resilienceTimeout = require("./resilience_timeout");
const options = {};
options.timeout = 10000;
options.onTimeout = function (req, res) {
let message = { errorCode: "TimeOutError",
errorMessage: "Your request could not processed within a timeframe" };
res.status(500).send(JSON.stringify(message));
};
cds.middlewares.add(resilienceTimeout.timeoutHandler(options));
module.exports = cds.server;
In the provided code snippet, timeout logic (resilience_timeout.js) is integrated into the CAP application as a middleware, accompanied by specific configurations. This is achieved by utilizing the add method from cds.middlewares.
Let’s proceed to understand how timeouts can be configured and applied to all requests.
// Default Options: Values and Functions
const DEFAULT_DISABLE_LIST = [ "setHeaders", "write", "send", "json", "status", "type",
"end", "writeHead", "addTrailers", "writeContinue", "append",
"attachment", "download", "format", "jsonp", "location",
"redirect", "render", "sendFile", "sendStatus", "set", "vary" ];
const DEFAULT_TIMEOUT = 60 * 1000;
const DEFAULT_ON_TIMEOUT = function (req, res){
res.status(503).send({error: 'Service is taking longer than expected. Please retry!'});
}
//Implementation: Functions and Handlers
initialiseOptions = (options)=>{
options = options || {};
if (options.timeout && (typeof options.timeout !== 'number' || options.timeout % 1 !== 0 || options.timeout <= 0)) {
throw new Error('Timeout option must be a whole number greater than 0!');
}
if (options.onTimeout && typeof options.onTimeout !== 'function') {
throw new Error('onTimeout option must be a function!');
}
if (options.disable && !Array.isArray(options.disable)) {
throw new Error('disable option must be an array!');
}
options.timeout = options.timeout || DEFAULT_TIMEOUT;
options.onTimeout = options.onTimeout || DEFAULT_ON_TIMEOUT;
options.disableList = options.disableList || DEFAULT_DISABLE_LIST;
return options;
}
timeoutMiddleware = function (req, res, next){
req.connection.setTimeout(this.config.timeout);
res.on('timeout', (socket)=>{
if (!res.headersSent) {
this.config.onTimeout(req, res, next);
this.config.disableList.forEach( method => {
res[method] = function(){console.error(`ERROR: ${method} was called after TimeOut`)};
});
}
});
next();
}
timeoutHandler = (options)=>{
this.config = initialiseOptions(options);
return timeoutMiddleware.bind(this);
}
module.exports = {timeoutHandler};
In above code, there are 3 methods to note:
In Node.js, req.connection.setTimeout is a method used to set the timeout duration for a specific HTTP request connection. When you set a timeout using this method, you are defining the maximum amount of time the server will wait for activity on the connection. If no data is sent or received within the specified timeframe, the server will automatically terminate the connection and emits a timeout event.
Send a request by calling getData function with wait=true parameter then it will return a timeout error after period provided in configuration as shown below:
### Wait=TRUE, Timeout Happens
GET http://localhost:4004/odata/v4/my/getData(wait=true)
Response
It is important to understand that, even if a timeout is triggered, and the plugin sends a response, the processing of logic within CAP application service handlers does not stop. In the previous example, if a timeout happens and the request handler sends a timeout response to the client, it means that the request is terminated from the client’s perspective, but the asynchronous operations that were already initiated will continue to execute on the server.
If you want to stop asynchronous operations when a timeout occurs, you would need to implement additional logic to cancel or abort these operations. This might involve using libraries or methods specific to the asynchronous tasks you’re performing. For example, database libraries often provide mechanisms to cancel queries, and HTTP request libraries might have options to abort ongoing requests.
In Node.js, headersSent is a property of the response object that indicates whether the response headers have already been sent to the client. You can use this field to control some execution and also rollback some of earlier executions or transactions using tx.rollback(). More information is available here: srv.tx
It’s important to handle asynchronous operations carefully, especially in scenarios where timeouts and other errors can occur. Proper error handling and cleanup mechanisms should be implemented to ensure the stability and reliability of your application.