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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ Servers without the v2 API report errors without a code. For them, the client as

EventSourcingDB never stores a write halfway: the events of a call to `write_events`, or the schema of a call to `register_event_schema`, are either stored completely or not at all. If one of these functions fails in a way that leaves open whether the server stored the write, it raises an `OutcomeUnknownError`, whose `cause` attribute holds the error that the write failed with. This happens, for example, if the server stalls or the connection breaks once the request has a connection, if the server answers with a status such as `500`, `502`, or `504`, if the answer does not come from EventSourcingDB, or if the server ends its answer before it is complete. Before you repeat such a write, find out whether it was stored, for example by reading the events of the subject, or repeat it with a precondition such as `IsSubjectOnEventId` that only holds if it was not stored.

The function raises the error itself, which tells that the write was certainly not stored, only if the client refused the write before sending it, such as with an `IncompatibleServerError`, if the client could not find out which API the server offers, which it asks with a ping before the write where needed (see [Talking to Servers With and Without the v2 API](#talking-to-servers-with-and-without-the-v2-api)), for example because the ping stalled, the server answered it with a status such as `500`, or the answer did not come from EventSourcingDB, if the request could not get a connection at all, such as when the name of the server cannot be resolved, connecting or TLS fails, or the request waits for a free connection for longer than the stall timeout, or if the server refused the write: with an error that it reports within its answer, other than `ErrorCode.STORE_FAILED`, with one of the statuses `400`, `401`, `408`, `413`, `429`, and `503`, or, for servers without the v2 API, with any status `4xx`, `503`, or `507`.
The function raises the error itself, which tells that the write was certainly not stored, only if the client refused the write before sending it, such as with an `IncompatibleServerError`, if the client could not find out which API the server offers, which it asks with a ping before the write where needed (see [Talking to Servers With and Without the v2 API](#talking-to-servers-with-and-without-the-v2-api)), for example because the ping stalled, the server answered it with a status such as `500`, or the answer did not come from EventSourcingDB, if the request could not get a connection at all, such as when the name of the server cannot be resolved, connecting or TLS fails, or the request waits for a free connection for longer than the stall timeout, or if the server refused the write: with an error that it reports within its answer, with one of the statuses `400`, `401`, `408`, `413`, `429`, and `503`, or, for servers without the v2 API, with any status `4xx`, `503`, or `507`.

Cancelling a write once it has a connection, for example with `asyncio.timeout` or by cancelling its task, leaves its outcome open as well. It is cancelled as usual nonetheless, so that `asyncio.timeout`, `asyncio.wait_for`, and task groups work as always: the `asyncio.CancelledError` is not wrapped, but marked, and carries a note that says so. To tell whether the outcome of a write is unknown, call `is_outcome_unknown` with the error. It returns `True` for an `OutcomeUnknownError`, for a marked `CancelledError`, and for an error caused by one of them, such as the `TimeoutError` of `asyncio.timeout`:

Expand Down
11 changes: 7 additions & 4 deletions eventsourcingdb/transport/v2/transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from ...errors import (
CustomError,
DbApiError,
ErrorCode,
InternalError,
ServerError,
ValidationError,
Expand Down Expand Up @@ -64,11 +63,15 @@
def is_refusal(error: DbApiError) -> bool:
"""
Tells whether the server refused a write before it stored anything, as
the v2 API does with every error item except store-failed, and with the
statuses above.
the v2 API does with every error item, and with the statuses above. This
includes store-failed, which the server reports only after its store has
failed, since it then refuses every later write until it is restarted. A
write during which storing fails gets an error without a code instead, a
status of 500 or a stream that ends before it is complete, so its outcome
is unknown.
"""
if error.status == HTTPStatus.OK:
return error.code != ErrorCode.STORE_FAILED
return True

return error.status in REFUSING_STATUSES

Expand Down
17 changes: 2 additions & 15 deletions tests/test_outcome_unknown.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,13 +282,12 @@ async def keep_busy(response: web.StreamResponse) -> None:
'license-invalid',
'store-read-only',
'disk-full',
'store-failed',
'indexes-rebuilding',
'server-shutting-down',
],
)
async def test_raises_an_error_item_of_the_v2_api_other_than_store_failed(
request_: Request, code: str
) -> None:
async def test_raises_an_error_item_of_the_v2_api(request_: Request, code: str) -> None:
error = await outcome_of(
request_, StreamServer(answer_with(V2_HEARTBEAT + error_item(code) + V2_END), api='v2')
)
Expand Down Expand Up @@ -328,18 +327,6 @@ async def test_raises_an_error_of_another_4xx_or_507_of_the_v1_api(


class TestWritesWhoseOutcomeIsUnknown:
@staticmethod
@pytest.mark.asyncio
@pytest.mark.parametrize('request_', [write.values[0] for write in WRITES])
async def test_reports_an_unknown_outcome_for_a_store_that_failed(request_: Request) -> None:
error = await outcome_of(
request_,
StreamServer(answer_with(error_item('store-failed') + V2_END), api='v2'),
)

assert_is_outcome_unknown(error, DbApiError)
assert error.cause.code == 'https://docs.eventsourcingdb.io/errors/store-failed' # type: ignore[union-attr]

@staticmethod
@pytest.mark.asyncio
@pytest.mark.parametrize(('api', 'request_'), WRITES_PER_API)
Expand Down
Loading