Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix BB-760 : When elasticsearch configuration is missing, server crashes #1081

Merged
merged 12 commits into from
May 27, 2024
Merged
8 changes: 0 additions & 8 deletions config/config.json.ctmpl
Original file line number Diff line number Diff line change
Expand Up @@ -54,21 +54,13 @@
{{if service "bookbrainz-elasticsearch-test"}}
{{with index (service "bookbrainz-elasticsearch-test") 0}}
"node": "http://{{.Address}}:{{.Port}}",
"auth": {
"username": "elastic",
"password": "changeme"
},
"requestTimeout": 30000
{{end}}
{{end}}
{{ else }}
{{if service "bookbrainz-elasticsearch"}}
{{with index (service "bookbrainz-elasticsearch") 0}}
"node": "http://{{.Address}}:{{.Port}}",
"auth": {
"username": "elastic",
"password": "changeme"
},
"requestTimeout": 30000
{{end}}
{{end}}
Expand Down
4 changes: 0 additions & 4 deletions config/config.json.example
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,6 @@
},
"search": {
"node": "http://elasticsearch:9200",
"auth": {
"username": "elastic",
"password": "changeme"
},
"requestTimeout": 30000
},
"mailConfig" :{
Expand Down
4 changes: 0 additions & 4 deletions config/config.local.json.example
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,6 @@
},
"search": {
"node": "http://localhost:9200",
"auth": {
"username": "elastic",
"password": "changeme"
},
"requestTimeout": 60000
}
}
1 change: 0 additions & 1 deletion src/api/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ mainRouter.use((req, res) => {
// https://github.com/elastic/elasticsearch-js/issues/33
search.init(app.locals.orm, Object.assign({}, config.search));


const DEFAULT_API_PORT = 9098;
app.set('port', process.env.PORT || DEFAULT_API_PORT);

Expand Down
2 changes: 1 addition & 1 deletion src/client/components/pages/entities/cbReviewModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,7 @@ class CBReviewModal extends React.Component<

/* executes getAccessToken() only in a browser to avoid unnecessary server-side calls during component mounting */
componentDidMount = async () => {
if (typeof window !== "undefined") {
if (typeof window !== 'undefined') {
this.accessToken = await this.getAccessToken();
}
};
Expand Down
6 changes: 3 additions & 3 deletions src/client/entity-editor/entity-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,10 @@ const EntityEditor = (props: Props) => {
window.onbeforeunload = handleUrlChange;
}, [handleUrlChange]);

if(entity){
entityURL = getEntityUrl(entity);
Tarunmeena0901 marked this conversation as resolved.
Show resolved Hide resolved
if (entity) {
entityURL = getEntityUrl(entity);
}

return (
<form onSubmit={onSubmit}>
<Card>
Expand Down
10 changes: 5 additions & 5 deletions src/client/entity-editor/identifier-editor/identifier-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ import {
} from '../validators/common';
import type {Dispatch} from 'redux';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import IdentifierLink from '../../components/pages/entities/identifiers-links.js';
import Select from 'react-select';
import ValueField from './value-field';
import {collapseWhiteSpaces} from '../../../common/helpers/utils';
import {connect} from 'react-redux';
import {faTimes} from '@fortawesome/free-solid-svg-icons';
import IdentifierLink from "../../components/pages/entities/identifiers-links.js"


type OwnProps = {
Expand Down Expand Up @@ -131,11 +131,11 @@ function IdentifierRow({
</Row>
{typeValue && valueValue && (
<Row>
<Col>
<Col>
Preview Link:
<IdentifierLink typeId={typeValue} value={valueValue}/>
</Col>
</Row>
<IdentifierLink typeId={typeValue} value={valueValue}/>
</Col>
</Row>
)}
<hr/>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/client/helpers/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ export function stringToHTMLWithLinks(content: string) {
cleanUrl = url.substring(0, firstUnbalancedParanthesis);
suffix = url.substring(firstUnbalancedParanthesis);
}
let link = `<a href="${cleanUrl.startsWith('www.') ? `https://${cleanUrl}` : cleanUrl}" target="_blank">${cleanUrl}</a>`;
const link = `<a href="${cleanUrl.startsWith('www.') ? `https://${cleanUrl}` : cleanUrl}" target="_blank">${cleanUrl}</a>`;
return link + suffix;
}
);
Expand Down
107 changes: 64 additions & 43 deletions src/common/helpers/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@

import * as commonUtils from './utils';
import {camelCase, isString, snakeCase, upperFirst} from 'lodash';

import ElasticSearch from '@elastic/elasticsearch';
import ElasticSearch, {ApiResponse, type Client, type ClientOptions} from '@elastic/elasticsearch';
import type {EntityTypeString} from 'bookbrainz-data/lib/types/entity';
import {type ORM} from 'bookbrainz-data';
import httpStatus from 'http-status';
import log from 'log';

Expand All @@ -33,7 +33,7 @@ const _bulkIndexSize = 10000;
const _retryDelay = 10;
const _maxJitter = 75;

let _client = null;
let _client:Client = null;

function sanitizeEntityType(type) {
if (!type) {
Expand Down Expand Up @@ -189,39 +189,45 @@ export async function _bulkIndexEntities(entities) {

operationSucceeded = true;

// eslint-disable-next-line no-await-in-loop
const response = await _client.bulk({
body: bulkOperations
}).catch(error => { log.error('error bulk indexing entities for search:', error); });

/*
* In case of failed index operations, the promise won't be rejected;
* instead, we have to inspect the response and respond to any failures
* individually.
*/
if (response?.errors === true) {
entitiesToIndex = response.items.reduce((accumulator, item) => {
// We currently only handle queue overrun
if (item.index.status === httpStatus.TOO_MANY_REQUESTS) {
const failedEntity = entities.find(
(element) => (element.bbid ?? element.id) === item.index._id
);

accumulator.push(failedEntity);
}
try {
// eslint-disable-next-line no-await-in-loop
const {body: bulkResponse} = await _client.bulk({
body: bulkOperations
});

/*
* In case of failed index operations, the promise won't be rejected;
* instead, we have to inspect the response and respond to any failures
* individually.
*/
if (bulkResponse?.errors === true) {
entitiesToIndex = bulkResponse.items.reduce((accumulator, item) => {
// We currently only handle queue overrun
if (item.index.status === httpStatus.TOO_MANY_REQUESTS) {
const failedEntity = entities.find(
(element) => (element.bbid ?? element.id) === item.index._id
);

accumulator.push(failedEntity);
}

return accumulator;
}, []);
return accumulator;
}, []);


if (entitiesToIndex.length) {
operationSucceeded = false;
if (entitiesToIndex.length) {
operationSucceeded = false;

const jitter = Math.random() * _maxJitter;
// eslint-disable-next-line no-await-in-loop
await new Promise(resolve => setTimeout(resolve, _retryDelay + jitter));
const jitter = Math.random() * _maxJitter;
// eslint-disable-next-line no-await-in-loop
await new Promise(resolve => setTimeout(resolve, _retryDelay + jitter));
}
}
}
catch (error) {
log.error('error bulk indexing entities for search:', error);
operationSucceeded = false;
}
}
}

Expand Down Expand Up @@ -587,22 +593,37 @@ export function searchByName(orm, name, type, size, from) {
return _searchForEntities(orm, dslQuery);
}

export async function init(orm, options) {
if (!isString(options.host)) {
options.host = 'localhost:9200';
/**
* Search init
* @description Sets up the search server connection with defaults,
* and returns a connection status boolean
* @param {ORM} orm the BookBrainz ORM
* @param {ClientOptions} [options] Optional (but recommended) connection settings, will provide defaults if missing
* @returns {Promise<boolean>} A Promise which resolves to the connection status boolean
*/
export async function init(orm: ORM, options:ClientOptions) {
if (!isString(options.node)) {
const defaultOptions:ClientOptions = {
node: 'http://localhost:9200',
requestTimeout: 60000
};
log.warning('ElasticSearch configuration not provided. Using default settings.');
_client = new ElasticSearch.Client(defaultOptions);
}
else {
_client = new ElasticSearch.Client(options);
}

_client = new ElasticSearch.Client(options);

// Automatically index on app startup if we haven't already
try {
const mainIndexExists = await _client.indices.exists({index: _index});
if (mainIndexExists) {
return null;
}
return generateIndex(orm);
await _client.ping();
}
catch (error) {
return null;
log.warning('Could not connect to ElasticSearch:', error.toString());
return false;
}
const mainIndexExists = await _client.indices.exists({index: _index});
if (!mainIndexExists) {
// Automatically index on app startup if we haven't already, but don't block app setup
generateIndex(orm).catch(log.error);
}
return true;
}
13 changes: 12 additions & 1 deletion src/server/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,13 @@ if (config.influx) {

// Authentication code depends on session, so init session first
const authInitiated = auth.init(app);
let searchInitiated;

// Clone search config to prevent error if starting webserver and api
// https://github.com/elastic/elasticsearch-js/issues/33
search.init(app.locals.orm, Object.assign({}, config.search));
(async function initializeSearch() {
searchInitiated = await search.init(app.locals.orm, Object.assign({}, config.search));
})();

// Set up constants that will remain valid for the life of the app
debug(`Git revision: ${siteRevision}`);
Expand All @@ -124,6 +127,14 @@ app.use((req, res, next) => {
});
}

if (!searchInitiated) {
const msg = 'We could not connect to our search server, all search functionality is unavailable.';
res.locals.alerts.push({
level: 'danger',
message: `${msg}`
});
}

if (!req.session || !authInitiated) {
res.locals.alerts.push({
level: 'danger',
Expand Down