Skip to content

Commit

Permalink
Merge "[INTERNAL] sap.ui.model.odata.v4.ODataModel#getRetryAfterPromi…
Browse files Browse the repository at this point in the history
…se:"
  • Loading branch information
ThomasChadzelek authored and Gerrit Code Review committed Nov 8, 2024
2 parents 5a8b2fa + 85a665f commit 9358699
Show file tree
Hide file tree
Showing 9 changed files with 463 additions and 197 deletions.
39 changes: 35 additions & 4 deletions src/sap.ui.core/src/sap/ui/model/odata/v4/ODataModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -355,10 +355,11 @@ sap.ui.define([
this.mMetadataHeaders = {"Accept-Language" : sLanguageTag};

mQueryParams = Object.assign({}, mUriParameters, mParameters.metadataUrlParams);
const fnGetOrCreateRetryAfterPromise = this.getOrCreateRetryAfterPromise.bind(this);
this.oMetaModel = new ODataMetaModel(
_MetadataRequestor.create(this.mMetadataHeaders, sODataVersion,
mParameters.ignoreAnnotationsFromMetadata, mQueryParams,
mParameters.withCredentials),
mParameters.withCredentials, fnGetOrCreateRetryAfterPromise),
this.sServiceUrl + "$metadata", mParameters.annotationURI, this,
mParameters.supportReferences, mQueryParams["sap-language"]);
this.oInterface = {
Expand All @@ -372,10 +373,8 @@ sap.ui.define([
getGroupProperty : this.getGroupProperty.bind(this),
getMessagesByPath : this.getMessagesByPath.bind(this),
getOptimisticBatchEnabler : this.getOptimisticBatchEnabler.bind(this),
getOrCreateRetryAfterPromise : fnGetOrCreateRetryAfterPromise,
getReporter : this.getReporter.bind(this),
getRetryAfterHandler : function () {
return that.fnRetryAfter;
},
isIgnoreETag : function () {
return that.bIgnoreETag;
},
Expand Down Expand Up @@ -428,6 +427,7 @@ sap.ui.define([
// ensure the events are respectively fired once for a GET request
this.mPath2DataRequestedCount = {};
this.fnRetryAfter = null;
this.oRetryAfterPromise = null;
}

/**
Expand Down Expand Up @@ -1642,6 +1642,8 @@ sap.ui.define([
this.oRequestor.destroy();
this.mHeaders = undefined;
this.mMetadataHeaders = undefined;
this.oRetryAfterPromise = undefined;

return Model.prototype.destroy.apply(this, arguments);
};

Expand Down Expand Up @@ -2092,6 +2094,35 @@ sap.ui.define([
return this.fnOptimisticBatchEnabler;
};

/**
* Returns the promise that is currently being used for "Retry-After" handling. Returns
* <code>null</code> if no "Retry-After" is currently known. Creates a new promise if there is
* none, an error is given, and a {@link #setRetryAfterHandler handler} is known.
*
* @param {Error} [oRetryAfterError] - A "Retry-After" error from a backend call
* @returns {Promise|null} The current "Retry-After" promise
*
* @private
*/
ODataModel.prototype.getOrCreateRetryAfterPromise = function (oRetryAfterError) {
if (!this.oRetryAfterPromise && this.fnRetryAfter && oRetryAfterError) {
this.oRetryAfterPromise = this.fnRetryAfter(oRetryAfterError);
this.oRetryAfterPromise.finally(() => {
this.oRetryAfterPromise = null;
}).catch(() => { /* catch is only needed due to finally */ });
this.oRetryAfterPromise.catch((oError) => {
// own error reason is not reported to the message model
if (oError === oRetryAfterError) {
this.reportError(oError.message, sClassName, oError);
} else {
oError.$reported = true;
}
});
}

return this.oRetryAfterPromise;
};

/**
* Method not supported
*
Expand Down
80 changes: 49 additions & 31 deletions src/sap.ui.core/src/sap/ui/model/odata/v4/lib/_MetadataRequestor.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,20 @@ sap.ui.define([
* is deleted(!) after the first <code>read</code> for a metadata document.
* @param {boolean} [bWithCredentials]
* Whether the XHR should be called with <code>withCredentials</code>
* @param {function} fnGetOrCreateRetryAfterPromise
* A function that returns or creates the "Retry-After" promise
* @returns {object}
* A new MetadataRequestor object
*/
create : function (mHeaders, sODataVersion, bIgnoreAnnotationsFromMetadata, mQueryParams,
bWithCredentials) {
bWithCredentials, fnGetOrCreateRetryAfterPromise) {
var mUrl2Promise = {},
sQuery = _Helper.buildQuery(mQueryParams);

return {
/**
* Reads a metadata document from the given URL.
* Reads a metadata document from the given URL, taking care of "Retry-After".
*
* @param {string} sUrl
* The URL of a metadata document, it must not contain a query string or a
* fragment part
Expand Down Expand Up @@ -84,38 +87,53 @@ sap.ui.define([
delete mUrl2Promise[sUrl];
} else {
oPromise = new Promise(function (fnResolve, fnReject) {
const oAjaxSettings = {
method : "GET",
headers : mHeaders
};
if (bWithCredentials) {
oAjaxSettings.xhrFields = {withCredentials : true};
}
function send() {
const oAjaxSettings = {
method : "GET",
headers : mHeaders
};
if (bWithCredentials) {
oAjaxSettings.xhrFields = {withCredentials : true};
}
jQuery.ajax(bAnnotations ? sUrl : sUrl + sQuery, oAjaxSettings)
.then(function (oData, _sTextStatus, jqXHR) {
var sDate = jqXHR.getResponseHeader("Date"),
sETag = jqXHR.getResponseHeader("ETag"),
oJSON = {$XML : oData},
sLastModified = jqXHR.getResponseHeader("Last-Modified");

jQuery.ajax(bAnnotations ? sUrl : sUrl + sQuery, oAjaxSettings)
.then(function (oData, _sTextStatus, jqXHR) {
var sDate = jqXHR.getResponseHeader("Date"),
sETag = jqXHR.getResponseHeader("ETag"),
oJSON = {$XML : oData},
sLastModified = jqXHR.getResponseHeader("Last-Modified");
if (sDate) {
oJSON.$Date = sDate;
}
if (sETag) {
oJSON.$ETag = sETag;
}
if (sLastModified) {
oJSON.$LastModified = sLastModified;
}
fnResolve(oJSON);
}, function (jqXHR) {
var oError
= _Helper.createError(jqXHR, "Could not load metadata");

if (sDate) {
oJSON.$Date = sDate;
}
if (sETag) {
oJSON.$ETag = sETag;
}
if (sLastModified) {
oJSON.$LastModified = sLastModified;
}
fnResolve(oJSON);
}, function (jqXHR, _sTextStatus, _sErrorMessage) {
var oError = _Helper.createError(jqXHR, "Could not load metadata");
if (jqXHR.status === 503
&& jqXHR.getResponseHeader("Retry-After")
&& fnGetOrCreateRetryAfterPromise(oError)) {
fnGetOrCreateRetryAfterPromise().then(send, fnReject);
} else {
Log.error("GET " + sUrl, oError.message,
"sap.ui.model.odata.v4.lib._MetadataRequestor");
fnReject(oError);
}
});
}

Log.error("GET " + sUrl, oError.message,
"sap.ui.model.odata.v4.lib._MetadataRequestor");
fnReject(oError);
});
const oRetryAfterPromise = fnGetOrCreateRetryAfterPromise();
if (oRetryAfterPromise) {
oRetryAfterPromise.then(send, fnReject);
} else {
send();
}
if (!bAnnotations
&& mQueryParams && "sap-context-token" in mQueryParams) {
delete mQueryParams["sap-context-token"];
Expand Down
34 changes: 8 additions & 26 deletions src/sap.ui.core/src/sap/ui/model/odata/v4/lib/_Requestor.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ sap.ui.define([
this.oModelInterface = oModelInterface;
this.oOptimisticBatch = null; // optimistic batch processing off
this.sQueryParams = _Helper.buildQuery(mQueryParams); // Used for $batch and CSRF token only
this.oRetryAfterPromise = null;
this.mRunningChangeRequests = {}; // map from group ID to a SyncPromise[]
this.iSessionTimer = 0;
this.iSerialNumber = 0;
Expand Down Expand Up @@ -2080,26 +2079,9 @@ sap.ui.define([
send(true);
}, fnReject);
} else if (jqXHR.status === 503 && jqXHR.getResponseHeader("Retry-After")
&& (that.oRetryAfterPromise
|| that.oModelInterface.getRetryAfterHandler())) {
if (!that.oRetryAfterPromise) {
const oRetryAfterError = _Helper.createError(jqXHR, "");
that.oRetryAfterPromise = that.oModelInterface.getRetryAfterHandler()(
oRetryAfterError);
that.oRetryAfterPromise.finally(() => {
that.oRetryAfterPromise = null;
}).catch(() => { /* catch is only needed due to finally */ });
that.oRetryAfterPromise.catch((oError) => {
// own error reason is not reported to the message model
if (oError === oRetryAfterError) {
that.oModelInterface
.reportError(oError.message, sClassName, oError);
} else {
oError.$reported = true;
}
});
}
that.oRetryAfterPromise.then(send, fnReject);
&& that.oModelInterface.getOrCreateRetryAfterPromise(
_Helper.createError(jqXHR, ""))) {
that.oModelInterface.getOrCreateRetryAfterPromise().then(send, fnReject);
} else {
sMessage = "Communication error";
if (sContextId) {
Expand All @@ -2119,8 +2101,9 @@ sap.ui.define([
});
}

if (that.oRetryAfterPromise) {
that.oRetryAfterPromise.then(send, fnReject);
const oRetryAfterPromise = that.oModelInterface.getOrCreateRetryAfterPromise();
if (oRetryAfterPromise) {
oRetryAfterPromise.then(send, fnReject);
} else if (that.oSecurityTokenPromise && sMethod !== "GET") {
that.oSecurityTokenPromise.then(send);
} else {
Expand Down Expand Up @@ -2335,13 +2318,12 @@ sap.ui.define([
* @param {function} oModelInterface.getOptimisticBatchEnabler
* A function that returns a callback function which controls the optimistic batch handling,
* see also {@link sap.ui.model.odata.v4.ODataModel#setOptimisticBatchEnabler}
* @param {function} oModelInterface.getOrCreateRetryAfterPromise
* A function that returns or creates the "Retry-After" promise
* @param {function} oModelInterface.getReporter
* A catch handler function expecting an <code>Error</code> instance. This function will call
* {@link sap.ui.model.odata.v4.ODataModel#reportError} if the error has not been reported
* yet
* @param {function} oModelInterface.getRetryAfterHandler
* A function that returns the "Retry-After" handler,
* see also {@link sap.ui.model.odata.v4.ODataModel#setRetryAfterHandler}
* @param {function():boolean} oModelInterface.isIgnoreETag
* Tells whether an entity's ETag should be actively ignored (If-Match:*) for PATCH requests.
* @param {function} oModelInterface.onCreateGroup
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5500,7 +5500,7 @@ sap.ui.define([
// Note: "ab-CD" is derived from Localization.getLanguageTag here, not from mHeaders!
this.mock(_MetadataRequestor).expects("create")
.withExactArgs({"Accept-Language" : "ab-CD"}, "4.0", undefined,
{"sap-language" : "~sLanguage~"}, undefined);
{"sap-language" : "~sLanguage~"}, undefined, sinon.match.func);
const oCopyAnnotationsExpectation
= this.mock(ODataMetaModel.prototype).expects("_copyAnnotations")
.withExactArgs(sinon.match.same(oMetaModel));
Expand Down Expand Up @@ -5545,7 +5545,8 @@ sap.ui.define([
.withArgs(false + "/Foo1/ValueListService/").callThrough();
// observe metadataUrlParams NOT being passed along
this.mock(_MetadataRequestor).expects("create")
.withExactArgs({"Accept-Language" : "ab-CD"}, "4.0", undefined, {}, undefined);
.withExactArgs({"Accept-Language" : "ab-CD"}, "4.0", undefined, {}, undefined,
sinon.match.func);

// code under test
oSharedModel = oMetaModel.getOrCreateSharedModel("../ValueListService/$metadata",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9476,6 +9476,86 @@ sap.ui.define([
TestUtils.onRequest(null);
});

//*********************************************************************************************
// Scenario: "Retry-After" handling: A request for $metadata is the 1st to encounter 503.
// JIRA: CPOUI5ODATAV4-2770
QUnit.test('503, "Retry-After" handling: $metadata', async function (assert) {
const aBatchPayloads = [];
TestUtils.onRequest((sPayload, sRequestLine) => {
aBatchPayloads.push(sPayload || sRequestLine);
});

let bAnswerWith503 = false;
const oModel = this.createModel(sTeaBusi, {}, {
// initial GET works just fine
[sTeaBusi + "$metadata"] : {source : "odata/v4/data/metadata.xml"},
"/sap/opu/odata4/IWBEP/TEA/default/iwbep/tea_busi_product/0001/$metadata"
: [{ // cross-service reference may fail at first try
code : 503,
headers : {"Retry-After" : "42"},
ifMatch : () => bAnswerWith503,
message : {
error : {code : "DB_MIGRATION", message : "DB migration in progress"}
}
}, {
source : "odata/v4/data/metadata_tea_busi_product.xml"
}]
});
oModel.$keepSend = true; // do not stub sendBatch/-Request

let iCallbackCount = 0;
let fnResolveRetryAfter;
oModel.setRetryAfterHandler((oError) => {
iCallbackCount += 1;
assert.ok(oError instanceof Error);
assert.strictEqual(oError.message, "DB migration in progress");
const iSeconds = (oError.retryAfter.getTime() - Date.now()) / 1000;
assert.ok(iSeconds > 41 && iSeconds < 43, `${iSeconds} roughly 42 seconds`);
return new Promise((resolve) => {
fnResolveRetryAfter = resolve;
});
});

await this.createView(assert, "", oModel);

assert.deepEqual(aBatchPayloads, [], "no requests yet");

assert.strictEqual(
await oModel.getMetaModel().requestObject("/TEAMS/MEMBER_COUNT/$Type"),
"Edm.Int32");

assert.strictEqual(aBatchPayloads.shift(), "GET " + sTeaBusi + "$metadata",
"main $metadata");
assert.deepEqual(aBatchPayloads, []);

bAnswerWith503 = true;

// code under test
const oPromise = oModel.getMetaModel().requestObject(
"/TEAMS/TEAM_2_EMPLOYEES/EMPLOYEE_2_EQUIPMENTS/EQUIPMENT_2_PRODUCT/ID/$Type");

await resolveLater(undefined, /*iDelay*/10); // autoRespondAfter defaults to 10ms

const sExpectedRequestLine
= "GET /sap/opu/odata4/IWBEP/TEA/default/iwbep/tea_busi_product/0001/$metadata";
assert.strictEqual(aBatchPayloads.shift(), sExpectedRequestLine,
"cross-service reference $metadata failed at first try");
assert.deepEqual(aBatchPayloads, []);
assert.strictEqual(iCallbackCount, 1);

bAnswerWith503 = false;
fnResolveRetryAfter();

assert.strictEqual(await oPromise, "Edm.Int32");

assert.strictEqual(aBatchPayloads.shift(), sExpectedRequestLine,
"cross-service reference $metadata succeeded at second try");
assert.deepEqual(aBatchPayloads, []);
assert.strictEqual(iCallbackCount, 1, "not called again");

TestUtils.onRequest(null);
});

//*********************************************************************************************
// Scenario: Table gets a binding context for which data was already loaded and then a refresh
// is performed synchronously.
Expand Down
Loading

0 comments on commit 9358699

Please sign in to comment.