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
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ public final class ErrorCode {
/** The code {@code restore-in-progress}: a restore is in progress, so the database does not accept the request. */
public static final String RESTORE_IN_PROGRESS = BASE_URL + "restore-in-progress";

/** The code {@code store-failed}: the database failed unexpectedly while it handled the request. */
/** The code {@code store-failed}: the store has failed before, so it refuses writes until the database is restarted. */
public static final String STORE_FAILED = BASE_URL + "store-failed";

/** The code {@code indexes-rebuilding}: the database is rebuilding its indexes, so it does not answer the request yet. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,18 @@ final class Outcomes {
return (status >= 400 && status < 500) || status == 503 || status == 507;
};

// The v2 API refuses a write it accepted with an error in its answer,
// except for store-failed, which reports that storing it failed halfway.
// It refuses a write it did not accept with a status code: 400 for a body
// of the wrong shape, 401 for an invalid API token, 408 for a body that
// did not arrive in time, 413 for a body that is too large, 429 for too
// many requests, and 503 for a server that is shutting down.
// The v2 API refuses a write it accepted with an error in its answer, and
// a write it did not accept with a status code: 400 for a body of the
// wrong shape, 401 for an invalid API token, 408 for a body that did not
// arrive in time, 413 for a body that is too large, 429 for too many
// requests, and 503 for a server that is shutting down.
// The error store-failed is a refusal, too: the server sends it only for
// writes that it refuses before storing them, once its store has failed.
// A write during which storing fails gets a 500, or a stream that ends
// without its end item, instead.
static final Predicate<DbApiException> V2_REFUSALS = exception -> {
if (exception.status() == 200) {
return !exception.code().equals(ErrorCode.STORE_FAILED);
}

return Set.of(400, 401, 408, 413, 429, 503).contains(exception.status());
var status = exception.status();
return status == 200 || Set.of(400, 401, 408, 413, 429, 503).contains(status);
};

private Outcomes() {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,15 @@ static Stream<Arguments> v2Writes() {
return Operations.writes().stream().map(Arguments::of);
}

// The server sends store-failed only for writes that it refuses before
// storing them, once its store has failed, so it is a refusal like any
// other error.
static Stream<Arguments> v2RefusalsWithAnError() {
return Operations.writes().stream()
.flatMap(operation -> Stream.of(ErrorCode.PRECONDITION_VIOLATED, ErrorCode.STORE_FAILED)
.map(code -> Arguments.of(operation, code)));
}

private static Client clientWith(FakeServer server) {
return new Client(server.url(), "secret", new ClientOptions().withStallTimeout(STALL_TIMEOUT));
}
Expand Down Expand Up @@ -127,34 +136,21 @@ void reportsAWriteThatTheServerAnswersWithAnotherStatusAsUnknown(ApiVersion api,
// The v2 API reports the errors of a write that it accepted within the
// stream that answers it, after heartbeats, if the write takes long.
@ParameterizedTest
@MethodSource("v2Writes")
void reportsAWriteThatTheV2ApiRefusesWithAnErrorAsNotStored(Operation operation) {
var error = ApiVersion.V2.item(
"error",
"{\"code\":\"" + ErrorCode.PRECONDITION_VIOLATED + "\",\"description\":\"refused\",\"details\":{}}");
@MethodSource("v2RefusalsWithAnError")
void reportsAWriteThatTheV2ApiRefusesWithAnErrorAsNotStored(Operation operation, String code) {
var error =
ApiVersion.V2.item("error", "{\"code\":\"" + code + "\",\"description\":\"refused\",\"details\":{}}");

try (var server = FakeServer.streaming(ApiVersion.V2, Lines.heartbeat(ApiVersion.V2), error)) {
var exception =
assertThrows(DbApiException.class, () -> operation.call().accept(clientWith(server)));

assertEquals(200, exception.status());
assertEquals(ErrorCode.PRECONDITION_VIOLATED, exception.code());
assertEquals(code, exception.code());
assertEquals("refused", exception.description());
}
}

// store-failed reports that storing failed halfway, so it may have been
// stored nevertheless.
@ParameterizedTest
@MethodSource("v2Writes")
void reportsAWriteThatTheV2ApiFailedToStoreAsUnknown(Operation operation) {
try (var server = FakeServer.streaming(ApiVersion.V2, Lines.error(ApiVersion.V2))) {
var cause = assertOutcomeUnknown(DbApiException.class, operation, clientWith(server));

assertEquals(ErrorCode.STORE_FAILED, cause.code());
}
}

@ParameterizedTest
@MethodSource("v2Writes")
void reportsAWriteWhoseAnswerOfTheV2ApiEndsWithoutItsEndItemAsUnknown(Operation operation) {
Expand Down
Loading