AllocZero / EfficientDynamoDb

High-performance C# DynamoDb library
https://alloczero.github.io/EfficientDynamoDb/
MIT License
204 stars 19 forks source link

Feature request: Provide a way to avoid throwing `ConditionalCheckFailedException` #251

Open bill-poole opened 3 months ago

bill-poole commented 3 months ago

When performing a conditional write (i.e., when specifying a condition expression for PutItem, UpdateItem or DeleteItem), the ExecuteAsync method currently throws a ConditionalCheckFailedException if the condition is not met. However, exceptions are quite slow in .NET because it is assumed that exceptions are exceptional - i.e., do not occur very often. However, that is not always the case when performing a conditional write.

For example, I have an application that writes data in batches, where each batch has a timestamp, and each record is only written if the timestamp of the existing record is earlier than that of the record being written. Furthermore, a single batch can require writing hundreds of thousands of records. If a batch fails towards the end and is re-executed, then most of the records in the batch will be rewritten and will fail the check condition.

Making matters more challenging is that exceptions are REALLY slow when debugging (over 100x slower in my experience), so I often have to wait 10-20 minutes or so on each debugging session before the condition I have breakpointed actually occurs because I'm waiting for all the prior database write operations to complete that fail their check conditions because the record being written was already previously written in the last run.

Therefore, it would be great if the library provided an option to handle condition check failures without throwing an exception. One way to achieve this would be to add a method (e.g., SuppressThrowing) to the relevant request builders that modified the return type of the ExecuteAsync method to be a result object (e.g., Task<PutItemResult>), where the result object could then be inspected to determine the success/failure of the operation.

The result object could also have a convenience method (e.g., ThrowIfFailed or EnsureSuccess) that throws an exception if an error occurred. This would allow callers to check the object for any conditions they want to explicitly handle and then optionally invoke the EnsureSuccess method.

So, a PutItem operation might look like:

var result = await _ddbContext.PutItem().WithItem(ddbItem)
    .WithCondition(filter => Joiner.Or(
    filter.On(item => item.Timestamp).NotExists(),
    filter.On(item => item.Timestamp).LessThan(ddbItem.Timestamp)))
    .SuppressThrowing()
    .ExecuteAsync(cancellationToken);

if (result.ExceptionType != ExceptionType.ConditionalCheckFailedException)
{
    result.EnsureSuccess();
}