[CouchDB] Better determination of indicator status (#5415)

* Add unknown state, remove maintenance state

* Handle all CouchDB status codes

- Set unknown status if we receive an unhandled code

* Include status code in error messages

* SharedWorker can send unknown status

* Add test for unknown status
This commit is contained in:
Jesse Mazzella 2022-06-30 09:30:32 -07:00 committed by GitHub
parent e8df2bd437
commit 0a2e0a4e65
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 117 additions and 25 deletions

View File

@ -93,8 +93,7 @@
message.state = 'close';
break;
default:
// Assume connection is closed
message.state = 'close';
message.state = 'unknown';
console.error('🚨 Received unexpected readyState value from CouchDB EventSource feed: 🚨', readyState);
break;
}

View File

@ -22,7 +22,7 @@
import CouchDocument from "./CouchDocument";
import CouchObjectQueue from "./CouchObjectQueue";
import { PENDING, CONNECTED, DISCONNECTED } from "./CouchStatusIndicator";
import { PENDING, CONNECTED, DISCONNECTED, UNKNOWN } from "./CouchStatusIndicator";
import { isNotebookType } from '../../notebook/notebook-constants.js';
const REV = "_rev";
@ -112,7 +112,7 @@ class CouchObjectProvider {
* Takes in a state message from the CouchDB SharedWorker and returns an IndicatorState.
* @private
* @param {'open'|'close'|'pending'} message
* @returns import('./CouchStatusIndicator').IndicatorState
* @returns {import('./CouchStatusIndicator').IndicatorState}
*/
#messageToIndicatorState(message) {
let state;
@ -126,14 +126,52 @@ class CouchObjectProvider {
case 'pending':
state = PENDING;
break;
default:
state = PENDING;
case 'unknown':
state = UNKNOWN;
break;
}
return state;
}
/**
* Takes an HTTP status code and returns an IndicatorState
* @private
* @param {number} statusCode
* @returns {import("./CouchStatusIndicator").IndicatorState}
*/
#statusCodeToIndicatorState(statusCode) {
let state;
switch (statusCode) {
case CouchObjectProvider.HTTP_OK:
case CouchObjectProvider.HTTP_CREATED:
case CouchObjectProvider.HTTP_ACCEPTED:
case CouchObjectProvider.HTTP_NOT_MODIFIED:
case CouchObjectProvider.HTTP_BAD_REQUEST:
case CouchObjectProvider.HTTP_UNAUTHORIZED:
case CouchObjectProvider.HTTP_FORBIDDEN:
case CouchObjectProvider.HTTP_NOT_FOUND:
case CouchObjectProvider.HTTP_METHOD_NOT_ALLOWED:
case CouchObjectProvider.HTTP_NOT_ACCEPTABLE:
case CouchObjectProvider.HTTP_CONFLICT:
case CouchObjectProvider.HTTP_PRECONDITION_FAILED:
case CouchObjectProvider.HTTP_REQUEST_ENTITY_TOO_LARGE:
case CouchObjectProvider.HTTP_UNSUPPORTED_MEDIA_TYPE:
case CouchObjectProvider.HTTP_REQUESTED_RANGE_NOT_SATISFIABLE:
case CouchObjectProvider.HTTP_EXPECTATION_FAILED:
case CouchObjectProvider.HTTP_SERVER_ERROR:
state = CONNECTED;
break;
case CouchObjectProvider.HTTP_SERVICE_UNAVAILABLE:
state = DISCONNECTED;
break;
default:
state = UNKNOWN;
}
return state;
}
//backwards compatibility, options used to be a url. Now it's an object
#normalize(options) {
if (typeof options === 'string') {
@ -163,21 +201,11 @@ class CouchObjectProvider {
let response = null;
try {
response = await fetch(this.url + '/' + subPath, fetchOptions);
this.indicator.setIndicatorToState(CONNECTED);
const { status } = response;
const json = await response.json();
this.#handleResponseCode(status, json, fetchOptions);
if (response.status === CouchObjectProvider.HTTP_CONFLICT) {
throw new this.openmct.objects.errors.Conflict(`Conflict persisting ${fetchOptions.body.name}`);
} else if (response.status === CouchObjectProvider.HTTP_BAD_REQUEST
|| response.status === CouchObjectProvider.HTTP_UNAUTHORIZED
|| response.status === CouchObjectProvider.HTTP_NOT_FOUND
|| response.status === CouchObjectProvider.HTTP_PRECONDITION_FAILED) {
const error = await response.json();
throw new Error(`CouchDB Error: "${error.error}: ${error.reason}"`);
} else if (response.status === CouchObjectProvider.HTTP_SERVER_ERROR) {
throw new Error('CouchDB Error: "500 Internal Server Error"');
}
return await response.json();
return json;
} catch (error) {
// Network error, CouchDB unreachable.
if (response === null) {
@ -188,6 +216,24 @@ class CouchObjectProvider {
}
}
/**
* Handle the response code from a CouchDB request.
* Sets the CouchDB indicator status and throws an error if needed.
* @private
*/
#handleResponseCode(status, json, fetchOptions) {
this.indicator.setIndicatorToState(this.#statusCodeToIndicatorState(status));
if (status === CouchObjectProvider.HTTP_CONFLICT) {
throw new this.openmct.objects.errors.Conflict(`Conflict persisting ${fetchOptions.body.name}`);
} else if (status >= CouchObjectProvider.HTTP_BAD_REQUEST) {
if (!json.error || !json.reason) {
throw new Error(`CouchDB Error ${status}`);
}
throw new Error(`CouchDB Error ${status}: "${json.error} - ${json.reason}"`);
}
}
/**
* Check the response to a create/update/delete request;
* track the rev if it's valid, otherwise return false to
@ -627,11 +673,25 @@ class CouchObjectProvider {
}
}
// https://docs.couchdb.org/en/3.2.0/api/basics.html
CouchObjectProvider.HTTP_OK = 200;
CouchObjectProvider.HTTP_CREATED = 201;
CouchObjectProvider.HTTP_ACCEPTED = 202;
CouchObjectProvider.HTTP_NOT_MODIFIED = 304;
CouchObjectProvider.HTTP_BAD_REQUEST = 400;
CouchObjectProvider.HTTP_UNAUTHORIZED = 401;
CouchObjectProvider.HTTP_FORBIDDEN = 403;
CouchObjectProvider.HTTP_NOT_FOUND = 404;
CouchObjectProvider.HTTP_METHOD_NOT_ALLOWED = 404;
CouchObjectProvider.HTTP_NOT_ACCEPTABLE = 406;
CouchObjectProvider.HTTP_CONFLICT = 409;
CouchObjectProvider.HTTP_PRECONDITION_FAILED = 412;
CouchObjectProvider.HTTP_REQUEST_ENTITY_TOO_LARGE = 413;
CouchObjectProvider.HTTP_UNSUPPORTED_MEDIA_TYPE = 415;
CouchObjectProvider.HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
CouchObjectProvider.HTTP_EXPECTATION_FAILED = 417;
CouchObjectProvider.HTTP_SERVER_ERROR = 500;
// If CouchDB is containerized via Docker it will return 503 if service is unavailable.
CouchObjectProvider.HTTP_SERVICE_UNAVAILABLE = 503;
export default CouchObjectProvider;

View File

@ -56,10 +56,10 @@ export const DISCONNECTED = {
description: "CouchDB is offline and unavailable for requests."
};
/** @type {IndicatorState} */
export const MAINTENANCE = {
statusClass: "s-status-warning-lo",
text: "CouchDB is in maintenance mode",
description: "CouchDB is online, but not currently accepting requests."
export const UNKNOWN = {
statusClass: "s-status-info",
text: "CouchDB connectivity unknown",
description: "CouchDB is in an unknown state of connectivity."
};
export default class CouchStatusIndicator {

View File

@ -25,7 +25,7 @@ import {
createOpenMct,
resetApplicationState, spyOnBuiltins
} from 'utils/testing';
import { CONNECTED, DISCONNECTED, PENDING } from './CouchStatusIndicator';
import { CONNECTED, DISCONNECTED, PENDING, UNKNOWN } from './CouchStatusIndicator';
describe('the plugin', () => {
let openmct;
@ -397,5 +397,38 @@ describe('the view', () => {
assertCouchIndicatorStatus(PENDING);
});
it("to 'unknown'", async () => {
const workerMessage = {
data: {
type: 'state',
state: 'unknown'
}
};
mockPromise = Promise.resolve({
status: 200,
json: () => {
return {
ok: true,
_id: 'some-value',
id: 'some-value',
_rev: 1,
model: {}
};
}
});
fetch.and.returnValue(mockPromise);
await openmct.objects.get({
namespace: '',
key: 'object-1'
});
// Simulate 'pending' state from worker message
provider.onSharedWorkerMessage(workerMessage);
await Vue.nextTick();
assertCouchIndicatorStatus(UNKNOWN);
});
});
});