Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions resources/search.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ClientError, ServerError, Violation } from '../utility/errors/hdbError.ts';
import { ClientError, IndexRebuildingError, Violation } from '../utility/errors/hdbError.ts';
import { OVERFLOW_MARKER, MAX_SEARCH_KEY_LENGTH, SEARCH_TYPES } from '../utility/lmdb/terms.ts';
import { compareKeys, MAXIMUM_KEY } from 'ordered-binary';
import { SKIP } from '@harperfast/extended-iterable';
Expand Down Expand Up @@ -333,7 +333,7 @@ export function searchByIndex(
403
);
if (index?.isIndexing)
throw new ServerError(`"${attribute_name}" is not indexed yet, can not search for this attribute`, 503);
throw new IndexRebuildingError(`"${attribute_name}" is not indexed yet, can not search for this attribute`);
if (value === null && index && !index.indexNulls)
throw new ClientError(
`"${attribute_name}" is not indexed for nulls, index needs to be rebuilt to search for nulls, can not search for this attribute`,
Expand Down
9 changes: 8 additions & 1 deletion server/serverHelpers/serverHandlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,14 @@ function serverErrorHandler(error, req, resp) {
harperLogger[error.logLevel || 'info'](error);
if (error.statusCode) {
if (typeof error.http_resp_msg !== 'object') {
return resp.code(error.statusCode).send({ error: error.http_resp_msg || error.message });
const body = { error: error.http_resp_msg || error.message };
// surface the machine-readable signal for errors that declare retryability (e.g. a
// still-building index, IndexRebuildingError) so API callers can distinguish and retry
if (error.retryable !== undefined) {
body.code = error.code;
body.retryable = error.retryable;
}
return resp.code(error.statusCode).send(body);
}
return resp.code(error.statusCode).send(error.http_resp_msg);
}
Expand Down
7 changes: 7 additions & 0 deletions unitTests/resources/update-schema.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const { loadGQLSchema } = require('#src/resources/graphql');
const assert = require('assert');
const test_data = require('../testData');
const { transaction } = require('#src/resources/transaction');
const { IndexRebuildingError } = require('#src/utility/errors/hdbError');
describe('Update Schema', () => {
before(async function () {
setupTestDBPath();
Expand Down Expand Up @@ -40,6 +41,12 @@ describe('Update Schema', () => {
caught_error = error;
}
assert(caught_error?.message.includes('not indexed yet'));
// the in-progress-index error is a distinct, retryable type so callers can tell it apart
// from a permanent failure rather than mis-handling the generic 503 (#1355)
assert(caught_error instanceof IndexRebuildingError, 'expected an IndexRebuildingError while the index builds');
assert.equal(caught_error.statusCode, 503);
assert.equal(caught_error.code, 'INDEX_REBUILDING');
assert.equal(caught_error.retryable, true);
await tables.SchemaChanges.indexingOperation;
let records = [];
for await (let record of tables.SchemaChanges.search({ conditions: [{ attribute: 'state', value: 'UT' }] })) {
Expand Down
15 changes: 15 additions & 0 deletions unitTests/server/serverHelpers/serverHandlers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,21 @@ describe('Test serverHandlers.js module ', () => {
assert.ok(test_result.msg.error === test_error.message, 'Resp message should equal custom error message object');
});

it('Should include code and retryable in the body for a retryable error (e.g. a still-building index)', () => {
const test_error = testUtils.deepClone(TEST_ERR);
test_error.statusCode = 503;
test_error.message = '"path" is not indexed yet, can not search for this attribute';
test_error.code = 'INDEX_REBUILDING';
test_error.retryable = true;

const test_result = serverHandlers_rw.serverErrorHandler(test_error, {}, new TestMockResp());

assert.ok(test_result.status_code === 503, 'Resp status code should be 503');
assert.ok(test_result.msg.error === test_error.message, 'Resp should include the error message');
assert.ok(test_result.msg.code === 'INDEX_REBUILDING', 'Resp should include the machine-readable code');
assert.ok(test_result.msg.retryable === true, 'Resp should mark the error retryable');
});

it('Should send a response with custom message object when included when an object is passed', () => {
const test_error = { blah: 'Custom error message!' };

Expand Down
15 changes: 15 additions & 0 deletions unitTests/utility/errors/hdbError.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const assert = require('assert');
const { IndexRebuildingError, ServerError } = require('#src/utility/errors/hdbError');

describe('IndexRebuildingError', () => {
it('is a retryable 503 ServerError with a stable machine-readable code', () => {
const err = new IndexRebuildingError('"path" is not indexed yet, can not search for this attribute');
assert(err instanceof ServerError, 'should extend ServerError');
assert(err instanceof Error);
assert.equal(err.name, 'IndexRebuildingError');
assert.equal(err.statusCode, 503);
Comment thread
heskew marked this conversation as resolved.
assert.equal(err.code, 'INDEX_REBUILDING');
assert.equal(err.retryable, true);
assert.equal(err.message, '"path" is not indexed yet, can not search for this attribute');
});
});
19 changes: 19 additions & 0 deletions utility/errors/hdbError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,25 @@ export class ServerError extends Error {
}
}

/**
* Thrown when a query targets an attribute whose secondary index is still being (re)built. It is a
* distinct, retryable 503 so callers can tell a transient "index rebuilding" condition apart from a
* permanent failure and retry, rather than mis-handling the generic 503 (e.g. as a "no result").
* See issue #1355.
*/
export class IndexRebuildingError extends ServerError {
code: string;
retryable: boolean;
constructor(message: string) {
super(message, 503);
// Set name explicitly: ServerError/Error leave it as 'Error', so without this the stack
// trace, JSON serialization, and any caller keying on error.name would not identify it.
this.name = 'IndexRebuildingError';
this.code = 'INDEX_REBUILDING';
this.retryable = true;
}
Comment thread
heskew marked this conversation as resolved.
}

/**
* This handler method is used to effectively evaluate caught errors and either translates them into a custom HdbError or,
* if it is already a HdbError, just returns the error to continue being thrown up the stack
Expand Down