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).
Refrence Github Repository: cap_resilient_timeout
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: 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: |
Let’s look at different options to apply timeout resilience for incoming requests to CAP application.
→ Via Standalone Approuter
→ 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']; }) })
"workspaces":["resilience-plugin"]It is possible to release these plugins as npm package and use it in multiple applications.
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;
// 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 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.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
User | Count |
---|---|
21 | |
13 | |
12 | |
10 | |
8 | |
7 | |
6 | |
6 | |
6 | |
5 |