Add configurable max retries with exponential backoff

This commit is contained in:
2026-03-05 11:12:15 +10:30
parent 174dd76a46
commit 9857433246
3 changed files with 76 additions and 45 deletions
+3 -1
View File
@@ -40,6 +40,8 @@ Editor fields:
- `Name` (optional) - `Name` (optional)
- `What` (required): dataset/resource name, for example `sales_invoice_item` - `What` (required): dataset/resource name, for example `sales_invoice_item`
- `OrderBy` (optional): default sort expression - `OrderBy` (optional): default sort expression
- `MaxRetries` (optional, default `3`): number of retries for timeout/token errors
- `RetryBackoffMs` (optional, default `500`): base delay in milliseconds; each retry uses exponential backoff (`base * 2^attempt`)
- `APIKey` (required unless provided in `msg.apikey`) - `APIKey` (required unless provided in `msg.apikey`)
## Runtime Inputs (`msg`) ## Runtime Inputs (`msg`)
@@ -78,7 +80,7 @@ You can override behavior per message:
## Notes ## Notes
- Base URL is currently fixed to `https://myobsync.accede.com.au`. - Base URL is currently fixed to `https://myobsync.accede.com.au`.
- Timeout and token error responses are retried recursively. - Timeout and token error responses are retried using bounded exponential backoff.
## License ## License
+11 -2
View File
@@ -6,7 +6,9 @@
name: {value:""}, name: {value:""},
apikey: {value:""}, apikey: {value:""},
what: {value:""}, what: {value:""},
orderby: {value:""} orderby: {value:""},
maxRetries: {value:3},
retryBackoffMs: {value:500}
}, },
inputs: 1, inputs: 1,
outputs: 2, outputs: 2,
@@ -30,6 +32,14 @@
<label for="node-input-orderby"><i class="fa fa-lock"></i> OrderBy</label> <label for="node-input-orderby"><i class="fa fa-lock"></i> OrderBy</label>
<input type="text" id="node-input-orderby" placeholder="OrderBy"> <input type="text" id="node-input-orderby" placeholder="OrderBy">
</div> </div>
<div class="form-row">
<label for="node-input-maxRetries"><i class="fa fa-repeat"></i> MaxRetries</label>
<input type="number" id="node-input-maxRetries" min="0" placeholder="3">
</div>
<div class="form-row">
<label for="node-input-retryBackoffMs"><i class="fa fa-clock-o"></i> RetryBackoffMs</label>
<input type="number" id="node-input-retryBackoffMs" min="0" placeholder="500">
</div>
<div class="form-row"> <div class="form-row">
<label for="node-input-apikey"><i class="fa fa-lock"></i> APIKey</label> <label for="node-input-apikey"><i class="fa fa-lock"></i> APIKey</label>
<input type="text" id="node-input-apikey" placeholder="APIKey"> <input type="text" id="node-input-apikey" placeholder="APIKey">
@@ -39,4 +49,3 @@
<script type="text/html" data-help-name="odbcwritenow-get"> <script type="text/html" data-help-name="odbcwritenow-get">
<p>odbcwritenow downloader</p> <p>odbcwritenow downloader</p>
</script> </script>
+41 -21
View File
@@ -1,13 +1,22 @@
async function DoImport(msg, url, node) { function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function DoImport(msg, url, node, maxRetries, baseBackoffMs) {
console.log('>', url) console.log('>', url)
msg.nodata = false msg.nodata = false
delete msg.complete delete msg.complete
node.status({ fill: "blue", shape: "ring", text: `Fetching #${msg.page}: ${msg.what}` }) let datastr = ""
var datastr = "" let attempt = 0
while (attempt <= maxRetries) {
msg.retry = attempt
node.status({ fill: "blue", shape: "ring", text: `Fetching #${msg.page}: ${msg.what} (try ${attempt + 1}/${maxRetries + 1})` })
try { try {
const response = await fetch(url); const response = await fetch(url);
datastr = await response.text(); datastr = await response.text();
if (datastr.toLowerCase().includes("no data found")) { if (datastr.toLowerCase().includes("no data found")) {
console.log("no data found"); console.log("no data found");
msg.nodata = true msg.nodata = true
@@ -17,32 +26,39 @@ async function DoImport(msg, url, node) {
return node.send([null, msg]); return node.send([null, msg]);
} }
//Errors and retrys etc // Retry only for gateway timeout and token error responses.
if (datastr.toLowerCase().includes("timeout")) { if (datastr.toLowerCase().includes("timeout") || datastr.toLowerCase().includes("token error")) {
node.status({ fill: "red", shape: "ring", text: `#${msg.page}: MYOB Gateway Timeout` }) const isLastTry = attempt >= maxRetries
console.error("******* MYOB Gateway Timeout", msg.retry, msg.page, url) const kind = datastr.toLowerCase().includes("timeout") ? "MYOB Gateway Timeout" : "MYOB Token Error"
msg.retry = msg.retry + 1
return await DoImport(msg, url, node) if (isLastTry) {
node.status({ fill: "red", shape: "ring", text: `#${msg.page}: ${kind} (max retries reached)` })
return node.error(`${kind} after ${maxRetries + 1} attempts`, msg)
} }
if (datastr.toLowerCase().includes("token error")) {
node.status({ fill: "red", shape: "ring", text: `#${msg.page}: MYOB Token Error` }) const delayMs = Math.max(0, baseBackoffMs) * Math.pow(2, attempt)
console.error("******* MYOB Token Error", msg.retry, msg.page, url) node.status({ fill: "red", shape: "ring", text: `#${msg.page}: ${kind}, retrying in ${delayMs}ms` })
msg.retry = msg.retry + 1 console.error("*******", kind, attempt, msg.page, `retry in ${delayMs}ms`)
return await DoImport(msg, url, node) attempt += 1
await sleep(delayMs)
continue
} }
//--------------------------------- //---------------------------------
//Everything looks good // Everything looks good.
const data = JSON.parse(datastr); const data = JSON.parse(datastr);
msg.rows = data.length msg.rows = data.length
console.log("Page:", data.page, "Rows:", data.length) console.log("Page:", msg.page, "Rows:", data.length)
node.status({ fill: "green", shape: "dot", text: `#${msg.page}: ${msg.rows} Rows` }) node.status({ fill: "green", shape: "dot", text: `#${msg.page}: ${msg.rows} Rows` })
msg.payload = data; msg.payload = data;
node.send([msg, null]); return node.send([msg, null]);
} catch (error) { } catch (error) {
node.status({ fill: "red", shape: "ring", text: error.message }) node.status({ fill: "red", shape: "ring", text: error.message })
console.error(error.message) console.error(error.message)
console.log(datastr) console.log(datastr)
return
}
} }
} }
@@ -63,7 +79,12 @@ module.exports = function(RED) {
msg.page = page msg.page = page
msg.what = what msg.what = what
msg.retry = 0 const maxRetries = Number.isInteger(parseInt(config.maxRetries, 10))
? Math.max(0, parseInt(config.maxRetries, 10))
: 3
const backoffMs = Number.isInteger(parseInt(config.retryBackoffMs, 10))
? Math.max(0, parseInt(config.retryBackoffMs, 10))
: 500
var orderbystr = ""; var orderbystr = "";
var filtersstr = ""; var filtersstr = "";
@@ -87,11 +108,10 @@ module.exports = function(RED) {
} }
const url = `https://myobsync.accede.com.au/download/${what}/json/${page}?apikey=${apikey}${filtersstr}${orderbystr}`; const url = `https://myobsync.accede.com.au/download/${what}/json/${page}?apikey=${apikey}${filtersstr}${orderbystr}`;
DoImport(msg, url, node) DoImport(msg, url, node, maxRetries, backoffMs)
}); });
} }
RED.nodes.registerType("odbcwritenow-get", ODBCWriteNowGet); RED.nodes.registerType("odbcwritenow-get", ODBCWriteNowGet);
} }