For speed we sometimes return response to consumer before state is saved in DB. Sometimes (Mostly for our automated consumers) this can break because the want to make actions on the saved data before it is saved. I wrote this little helper
public async Task<TEntity> GetWithRetry<TEntity>(Expression<Func<TEntity, bool>> predicate, string errorOnTimeout) where TEntity : class
{
var stopwatch = new Stopwatch();
stopwatch.Start();
do
{
var entity = await _context.DbSet<TEntity>().FirstOrDefaultAsync(predicate);
if(entity == null)
await Task.Delay(100);
else
return entity;
} while(stopwatch.Elapsed < TimeSpan.FromSeconds(1000));
throw new Exception(errorOnTimeout);
}
Used like
var existingBooking = await GetWithRetry<Booking>(b => b.BookingKey == confirmCommand.BookingKey, "Booking key not found");
Any pitfalls? Task.Delay
should scale well since it returns the thread to the pool, and if the data exists in DB the first time around it should not give much more overhead than an extra wrapped task?
This is the current version of the code:
public async Task<TEntity> FirstAsync<TEntity>(Expression<Func<TEntity, bool>> predicate, string errorOnTimeout, TimeSpan? timeout = null) where TEntity : class
{
var entity = await FirstAsyncOrDefault(predicate, timeout);
if(entity != null) return entity;
throw new Exception(errorOnTimeout);
}
public async Task<TEntity> FirstOrDefaultAsync<TEntity>(Expression<Func<TEntity, bool>> predicate, TimeSpan? timeout = null) where TEntity : class
{
var stopwatch = new Stopwatch();
stopwatch.Start();
if(timeout == null)
timeout = TimeSpan.FromSeconds(1);
do
{
var entity = await DbSet<TEntity>().FirstOrDefaultAsync(predicate);
if(entity != null) return entity;
await Task.Delay(100);
} while(stopwatch.Elapsed < timeout);
return null;
}
Best Answer
If you just want retries with timeout, you can use a
for
loop instead ofStopwatch
.But
Seems like a better solution would be to add a call for your customers that only completes when the data is written. Understanding that blocking for IO is a performance problem, don't! Try returning a
Task
, perhaps withTaskCompletionSource<T>
. Then you can run the operation asynchronously without blocking and have the server wake back up to complete the call when theTask
completes.Example batching scenario:
When your data infrastructure gets around to saving the data, it notifies the caller(s) of completion:
This way, you still are not blocking for IO on the API call. But you can offer the customer a way to be sure their write is committed before performing other operations.
Or
You could use the
TaskCompletionSource<TEntity>
for reads, and place the reads in the same queue with writes, so that the reads are guaranteed to occur after writes.