From 2ea227ca0664ca47d1d6c654fce1725a4266b5cd Mon Sep 17 00:00:00 2001 From: igoroctaviano Date: Thu, 13 May 2021 09:06:11 -0300 Subject: [PATCH] Extract hook and add config and documentation --- .webpack/webpack.base.js | 6 ++ .../src/DICOMSR/handleStructuredReport.js | 3 + .../src/classes/metadata/StudyMetadata.js | 2 + .../core/src/studies/services/qido/studies.js | 2 + .../wado/retrieveMetadataLoaderAsync.js | 39 +------- .../wado/retrieveMetadataLoaderSync.js | 2 + platform/core/src/utils/dicomLoaderService.js | 2 + platform/core/src/utils/index.js | 3 + platform/core/src/utils/index.test.js | 1 + .../metadataProvider/fetchOverlayData.js | 2 + .../fetchPaletteColorLookupTableData.js | 2 + .../core/src/utils/xhrRetryRequestHook.js | 91 +++++++++++++++++++ yarn.lock | 28 +----- 13 files changed, 119 insertions(+), 64 deletions(-) create mode 100644 platform/core/src/utils/xhrRetryRequestHook.js diff --git a/.webpack/webpack.base.js b/.webpack/webpack.base.js index ea5c83aa4..59fe02ddf 100644 --- a/.webpack/webpack.base.js +++ b/.webpack/webpack.base.js @@ -85,6 +85,12 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => { 'process.env.USE_LOCIZE': JSON.stringify(process.env.USE_LOCIZE || ''), 'process.env.LOCIZE_PROJECTID': JSON.stringify(process.env.LOCIZE_PROJECTID || ''), 'process.env.LOCIZE_API_KEY': JSON.stringify(process.env.LOCIZE_API_KEY || ''), + /** DICOMWebClient Retry options */ + 'process.env.DICOMWEB_CLIENT_RETRY_RETRIES': JSON.stringify(process.env.DICOMWEB_CLIENT_RETRY_RETRIES || 5), + 'process.env.DICOMWEB_CLIENT_RETRY_FACTOR': JSON.stringify(process.env.DICOMWEB_CLIENT_RETRY_FACTOR || 3), + 'process.env.DICOMWEB_CLIENT_RETRY_MIN_TIMEOUT': JSON.stringify(process.env.DICOMWEB_CLIENT_RETRY_MIN_TIMEOUT || 1 * 1000), + 'process.env.DICOMWEB_CLIENT_RETRY_MAX_TIMEOUT': JSON.stringify(process.env.DICOMWEB_CLIENT_RETRY_MAX_TIMEOUT || 60 * 1000), + 'process.env.DICOMWEB_CLIENT_RETRY_RANDOMIZE': JSON.stringify(process.env.DICOMWEB_CLIENT_RETRY_RANDOMIZE || true), }), ], // Fix: https://github.com/webpack-contrib/css-loader/issues/447#issuecomment-285598881 diff --git a/platform/core/src/DICOMSR/handleStructuredReport.js b/platform/core/src/DICOMSR/handleStructuredReport.js index fcaa19890..5bb818038 100644 --- a/platform/core/src/DICOMSR/handleStructuredReport.js +++ b/platform/core/src/DICOMSR/handleStructuredReport.js @@ -6,6 +6,7 @@ import parseDicomStructuredReport from './parseDicomStructuredReport'; import parseMeasurementsData from './parseMeasurementsData'; import getAllDisplaySets from './utils/getAllDisplaySets'; import errorHandler from '../errorHandler'; +import getXHRRetryRequestHook from '../utils/xhrRetryRequestHook'; const VERSION_NAME = 'dcmjs-0.0'; const TRANSFER_SYNTAX_UID = '1.2.840.10008.1.2.1'; @@ -23,6 +24,7 @@ const retrieveMeasurementFromSR = async (series, studies, serverUrl) => { url: serverUrl, headers: DICOMWeb.getAuthorizationHeader(), errorInterceptor: errorHandler.getHTTPErrorHandler(), + requestHooks: [getXHRRetryRequestHook()], }; const dicomWeb = new api.DICOMwebClient(config); @@ -74,6 +76,7 @@ const stowSRFromMeasurements = async (measurements, serverUrl) => { url: serverUrl, headers: DICOMWeb.getAuthorizationHeader(), errorInterceptor: errorHandler.getHTTPErrorHandler(), + requestHooks: [getXHRRetryRequestHook()], }; const dicomWeb = new api.DICOMwebClient(config); diff --git a/platform/core/src/classes/metadata/StudyMetadata.js b/platform/core/src/classes/metadata/StudyMetadata.js index 2e6531f9a..582c84e24 100644 --- a/platform/core/src/classes/metadata/StudyMetadata.js +++ b/platform/core/src/classes/metadata/StudyMetadata.js @@ -12,6 +12,7 @@ import { isImage } from '../../utils/isImage'; import { isDisplaySetReconstructable, isSpacingUniform } from '../../utils/isDisplaySetReconstructable'; import errorHandler from '../../errorHandler'; import isLowPriorityModality from '../../utils/isLowPriorityModality'; +import getXHRRetryRequestHook from '../../utils/xhrRetryRequestHook'; class StudyMetadata extends Metadata { constructor(data, uid) { @@ -889,6 +890,7 @@ function _getDisplaySetFromSopClassModule( url: study.getData().wadoRoot, headers, errorInterceptor, + requestHooks: [getXHRRetryRequestHook()], }); let displaySet = plugin.getDisplaySetFromSeries( diff --git a/platform/core/src/studies/services/qido/studies.js b/platform/core/src/studies/services/qido/studies.js index 93cb292d4..7a66c88f4 100644 --- a/platform/core/src/studies/services/qido/studies.js +++ b/platform/core/src/studies/services/qido/studies.js @@ -2,6 +2,7 @@ import { api } from 'dicomweb-client'; import DICOMWeb from '../../../DICOMWeb/'; import errorHandler from '../../../errorHandler'; +import getXHRRetryRequestHook from '../../../utils/xhrRetryRequestHook'; /** * Creates a QIDO date string for a date range query @@ -118,6 +119,7 @@ export default function Studies(server, filter) { url: server.qidoRoot, headers: DICOMWeb.getAuthorizationHeader(server), errorInterceptor: errorHandler.getHTTPErrorHandler(), + requestHooks: [getXHRRetryRequestHook()], }; const dicomWeb = new api.DICOMwebClient(config); diff --git a/platform/core/src/studies/services/wado/retrieveMetadataLoaderAsync.js b/platform/core/src/studies/services/wado/retrieveMetadataLoaderAsync.js index b1bb79cfe..8e8830239 100644 --- a/platform/core/src/studies/services/wado/retrieveMetadataLoaderAsync.js +++ b/platform/core/src/studies/services/wado/retrieveMetadataLoaderAsync.js @@ -1,6 +1,5 @@ import { api } from 'dicomweb-client'; import dcmjs from 'dcmjs'; -import retry, { operation } from 'retry'; import DICOMWeb from '../../../DICOMWeb/'; import RetrieveMetadataLoader from './retrieveMetadataLoader'; import { sortStudySeries, sortingCriteria } from '../../sortStudy'; @@ -11,6 +10,7 @@ import { } from './studyInstanceHelpers'; import errorHandler from '../../../errorHandler'; +import { getXHRRetryRequestHook } from '../../../utils/xhrRetryRequestHook'; const { naturalizeDataset } = dcmjs.data.DicomMetaDictionary; @@ -77,42 +77,7 @@ export default class RetrieveMetadataLoaderAsync extends RetrieveMetadataLoader url: server.qidoRoot, headers: DICOMWeb.getAuthorizationHeader(server), errorInterceptor: errorHandler.getHTTPErrorHandler(), - requestHooks: [ - (request, metadata) => { - function faultTolerantRequestSend(...args) { - const operation = retry.operation({ - retries: 10, - }); - /** - * retries: 5, - * factor: 3, - * minTimeout: 1 * 1000, - * maxTimeout: 60 * 1000, - * randomize: true - */ - - operation.attempt(function(currentAttempt) { - const originalOnReadyStateChange = request.onreadystatechange; - - request.onreadystatechange = function() { - originalOnReadyStateChange.call(request); - if (request.status === 429 || request.status >= 500) { - operation.retry(new Error('Attempt failed!')); - } - }; - - console.debug(`${metadata.url} (attempt: ${currentAttempt})`); - request.open(metadata.method, metadata.url, true); - originalRequestSend.call(request, ...args); - }); - } - - const originalRequestSend = request.send; - request.send = faultTolerantRequestSend; - - return request; - }, - ], + requestHooks: [getXHRRetryRequestHook()], }); this.client = client; diff --git a/platform/core/src/studies/services/wado/retrieveMetadataLoaderSync.js b/platform/core/src/studies/services/wado/retrieveMetadataLoaderSync.js index 90d1ae988..eebea975e 100644 --- a/platform/core/src/studies/services/wado/retrieveMetadataLoaderSync.js +++ b/platform/core/src/studies/services/wado/retrieveMetadataLoaderSync.js @@ -4,6 +4,7 @@ import { createStudyFromSOPInstanceList } from './studyInstanceHelpers'; import RetrieveMetadataLoader from './retrieveMetadataLoader'; import errorHandler from '../../../errorHandler'; +import getXHRRetryRequestHook from '../../../utils/xhrRetryRequestHook'; /** * Class for sync load of study metadata. @@ -61,6 +62,7 @@ export default class RetrieveMetadataLoaderSync extends RetrieveMetadataLoader { url: server.wadoRoot, headers: DICOMWeb.getAuthorizationHeader(server), errorInterceptor: errorHandler.getHTTPErrorHandler(), + requestHooks: [getXHRRetryRequestHook()], }); this.client = client; diff --git a/platform/core/src/utils/dicomLoaderService.js b/platform/core/src/utils/dicomLoaderService.js index f0792129a..b354c61ff 100644 --- a/platform/core/src/utils/dicomLoaderService.js +++ b/platform/core/src/utils/dicomLoaderService.js @@ -4,6 +4,7 @@ import { api } from 'dicomweb-client'; import DICOMWeb from '../DICOMWeb'; import errorHandler from '../errorHandler'; +import getXHRRetryRequestHook from './xhrRetryRequestHook'; const getImageId = imageObj => { if (!imageObj) { @@ -66,6 +67,7 @@ const wadorsRetriever = ( url, headers, errorInterceptor, + requestHooks: [getXHRRetryRequestHook()], }; const dicomWeb = new api.DICOMwebClient(config); diff --git a/platform/core/src/utils/index.js b/platform/core/src/utils/index.js index 6e9ea3140..87d6e19fd 100644 --- a/platform/core/src/utils/index.js +++ b/platform/core/src/utils/index.js @@ -18,6 +18,7 @@ import isDicomUid from './isDicomUid'; import resolveObjectPath from './resolveObjectPath'; import * as hierarchicalListUtils from './hierarchicalListUtils'; import * as progressTrackingUtils from './progressTrackingUtils'; +import xhrRetryRequestHook from './xhrRetryRequestHook'; const utils = { guid, @@ -40,6 +41,7 @@ const utils = { resolveObjectPath, hierarchicalListUtils, progressTrackingUtils, + xhrRetryRequestHook, }; export { @@ -63,6 +65,7 @@ export { resolveObjectPath, hierarchicalListUtils, progressTrackingUtils, + xhrRetryRequestHook, }; export default utils; diff --git a/platform/core/src/utils/index.test.js b/platform/core/src/utils/index.test.js index 85eb38230..a3ea78aa3 100644 --- a/platform/core/src/utils/index.test.js +++ b/platform/core/src/utils/index.test.js @@ -23,6 +23,7 @@ describe('Top level exports', () => { 'resolveObjectPath', 'hierarchicalListUtils', 'progressTrackingUtils', + 'xhrRetryRequestHook', ].sort(); const exports = Object.keys(utils.default).sort(); diff --git a/platform/core/src/utils/metadataProvider/fetchOverlayData.js b/platform/core/src/utils/metadataProvider/fetchOverlayData.js index 11e4d99cd..42af12e04 100644 --- a/platform/core/src/utils/metadataProvider/fetchOverlayData.js +++ b/platform/core/src/utils/metadataProvider/fetchOverlayData.js @@ -4,6 +4,7 @@ import str2ab from '../str2ab'; import unpackOverlay from './unpackOverlay'; import errorHandler from '../../errorHandler'; +import getXHRRetryRequestHook from '../xhrRetryRequestHook'; export default async function fetchOverlayData(instance, server) { const OverlayDataPromises = []; @@ -69,6 +70,7 @@ async function _getOverlayData(tag, server) { url: server.wadoRoot, //BulkDataURI is absolute, so this isn't used headers: DICOMWeb.getAuthorizationHeader(server), errorInterceptor: errorHandler.getHTTPErrorHandler(), + requestHooks: [getXHRRetryRequestHook()], }; const dicomWeb = new api.DICOMwebClient(config); const options = { diff --git a/platform/core/src/utils/metadataProvider/fetchPaletteColorLookupTableData.js b/platform/core/src/utils/metadataProvider/fetchPaletteColorLookupTableData.js index 8aba64ec4..26c65df57 100644 --- a/platform/core/src/utils/metadataProvider/fetchPaletteColorLookupTableData.js +++ b/platform/core/src/utils/metadataProvider/fetchPaletteColorLookupTableData.js @@ -3,6 +3,7 @@ import DICOMWeb from '../../DICOMWeb'; import str2ab from '../str2ab'; import errorHandler from '../../errorHandler'; +import getXHRRetryRequestHook from '../xhrRetryRequestHook'; export default async function fetchPaletteColorLookupTableData( instance, @@ -141,6 +142,7 @@ function _getPaletteColor(server, paletteColorLookupTableData, lutDescriptor) { url: server.wadoRoot, //BulkDataURI is absolute, so this isn't used headers: DICOMWeb.getAuthorizationHeader(server), errorInterceptor: errorHandler.getHTTPErrorHandler(), + requestHooks: [getXHRRetryRequestHook()], }; const dicomWeb = new api.DICOMwebClient(config); const options = { diff --git a/platform/core/src/utils/xhrRetryRequestHook.js b/platform/core/src/utils/xhrRetryRequestHook.js new file mode 100644 index 000000000..4b109621e --- /dev/null +++ b/platform/core/src/utils/xhrRetryRequestHook.js @@ -0,0 +1,91 @@ +import retry from 'retry'; + +const defaultRetryOptions = { + retries: process.env.DICOMWEB_CLIENT_RETRY_RETRIES || 5, + factor: process.env.DICOMWEB_CLIENT_RETRY_FACTOR || 3, + minTimeout: process.env.DICOMWEB_CLIENT_RETRY_MIN_TIMEOUT || 1 * 1000, + maxTimeout: process.env.DICOMWEB_CLIENT_RETRY_MAX_TIMEOUT || 60 * 1000, + randomize: process.env.DICOMWEB_CLIENT_RETRY_RANDOMIZE || true, +}; + +let retryOptions = { ...defaultRetryOptions }; + +/** + * Request hook used to add retry functionality to XHR requests. + * + * @param {XMLHttpRequest} request XHR request instance + * @param {object} metadata Metadata about the request + * @param {object} metadata.url URL + * @param {object} metadata.method HTTP method + * @returns {XMLHttpRequest} request instance optionally modified + */ +const xhrRetryRequestHook = (request, metadata) => { + const { url, method } = metadata; + + function faultTolerantRequestSend(...args) { + const operation = retry.operation(retryOptions); + + operation.attempt(function operationAttempt(currentAttempt) { + const originalOnReadyStateChange = request.onreadystatechange; + + request.onreadystatechange = function onReadyStateChange() { + originalOnReadyStateChange.call(request); + if (request.status === 429 || request.status >= 500) { + const errorMessage = `Attempt to request ${url} failed.`; + const attemptFailedError = new Error(errorMessage); + operation.retry(attemptFailedError); + } + }; + + console.warn(`Requesting ${url}... (attempt: ${currentAttempt})`); + request.open(method, url, true); + originalRequestSend.call(request, ...args); + }); + } + + const originalRequestSend = request.send; + request.send = faultTolerantRequestSend; + + return request; +}; + +/** + * Returns a configured retry request hook function + * that can be used to add retry functionality to XHR request. + * + * Default options: + * retries: 5 + * factor: 3 + * minTimeout: 1 * 1000 + * maxTimeout: 60 * 1000 + * randomize: true + * + * @param {*} options + * @param {*} options.retires number of retries + * @param {*} options.factor factor + * @param {*} options.minTimeout the min timeout + * @param {*} options.maxTimeout the max timeout + * @param {*} options.randomize randomize + * @returns {function} the configured retry request function + */ +export const getXHRRetryRequestHook = (options = {}) => { + retryOptions = { ...defaultRetryOptions }; + if ('retries' in options) { + retryOptions.retries = options.retries; + } + if ('factor' in options) { + retryOptions.factor = options.factor; + } + if ('minTimeout' in options) { + retryOptions.minTimeout = options.minTimeout; + } + if ('maxTimeout' in options) { + retryOptions.maxTimeout = options.maxTimeout; + } + if ('randomize' in options) { + retryOptions.randomize = options.randomize; + } + return xhrRetryRequestHook; +}; + +export default getXHRRetryRequestHook; diff --git a/yarn.lock b/yarn.lock index db9b980d3..7d49192bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1247,34 +1247,13 @@ pirates "^4.0.0" source-map-support "^0.5.9" -"@babel/runtime@7.1.2": - version "7.1.2" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.1.2.tgz#81c89935f4647706fc54541145e6b4ecfef4b8e3" - integrity sha512-Y3SCjmhSupzFB6wcv1KmmFucH6gDVnI30WjOcicV10ju0cZjak3Jcs67YLIXBrmZYw1xCrVeJPbycFwrqNyxpg== - dependencies: - regenerator-runtime "^0.12.0" - -"@babel/runtime@7.6.0": - version "7.6.0" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.6.0.tgz#4fc1d642a9fd0299754e8b5de62c631cf5568205" - integrity sha512-89eSBLJsxNxOERC0Op4vd+0Bqm6wRMqMbFtV3i0/fbaWw/mJ8Q3eBvgX0G4SyrOOLCtbu98HspF8o09MRT+KzQ== - dependencies: - regenerator-runtime "^0.13.2" - -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5": +"@babel/runtime@7.1.2", "@babel/runtime@7.5.5", "@babel/runtime@7.6.0", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.6": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132" integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ== dependencies: regenerator-runtime "^0.13.2" -"@babel/runtime@^7.6.0", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.6": - version "7.14.0" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.0.tgz#46794bc20b612c5f75e62dd071e24dfd95f1cbe6" - integrity sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA== - dependencies: - regenerator-runtime "^0.13.4" - "@babel/template@^7.0.0", "@babel/template@^7.1.0", "@babel/template@^7.4.0", "@babel/template@^7.4.4", "@babel/template@^7.6.0": version "7.6.0" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.6.0.tgz#7f0159c7f5012230dad64cca42ec9bdb5c9536e6" @@ -16544,11 +16523,6 @@ regenerator-runtime@^0.11.0, regenerator-runtime@^0.11.1: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== -regenerator-runtime@^0.12.0: - version "0.12.1" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de" - integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg== - regenerator-runtime@^0.13.1, regenerator-runtime@^0.13.2, regenerator-runtime@^0.13.4: version "0.13.5" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697"