From 0a2e0a4e65894b00038561f7062f2c5d8c772ef6 Mon Sep 17 00:00:00 2001 From: Jesse Mazzella Date: Thu, 30 Jun 2022 09:30:32 -0700 Subject: [PATCH] [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 --- .../persistence/couch/CouchChangesFeed.js | 3 +- .../persistence/couch/CouchObjectProvider.js | 96 +++++++++++++++---- .../persistence/couch/CouchStatusIndicator.js | 8 +- src/plugins/persistence/couch/pluginSpec.js | 35 ++++++- 4 files changed, 117 insertions(+), 25 deletions(-) diff --git a/src/plugins/persistence/couch/CouchChangesFeed.js b/src/plugins/persistence/couch/CouchChangesFeed.js index 1adf72eef3..3c1445cec7 100644 --- a/src/plugins/persistence/couch/CouchChangesFeed.js +++ b/src/plugins/persistence/couch/CouchChangesFeed.js @@ -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; } diff --git a/src/plugins/persistence/couch/CouchObjectProvider.js b/src/plugins/persistence/couch/CouchObjectProvider.js index 85ecb90471..b73d18fb3f 100644 --- a/src/plugins/persistence/couch/CouchObjectProvider.js +++ b/src/plugins/persistence/couch/CouchObjectProvider.js @@ -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; diff --git a/src/plugins/persistence/couch/CouchStatusIndicator.js b/src/plugins/persistence/couch/CouchStatusIndicator.js index e6599c4af7..069a59e220 100644 --- a/src/plugins/persistence/couch/CouchStatusIndicator.js +++ b/src/plugins/persistence/couch/CouchStatusIndicator.js @@ -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 { diff --git a/src/plugins/persistence/couch/pluginSpec.js b/src/plugins/persistence/couch/pluginSpec.js index 404e332de8..751f56e424 100644 --- a/src/plugins/persistence/couch/pluginSpec.js +++ b/src/plugins/persistence/couch/pluginSpec.js @@ -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); + }); }); });