Attendi stato asincrono

2

Per velocità a volte restituiamo la risposta al consumatore prima che lo stato venga salvato nel DB. A volte (principalmente per i nostri utenti automatizzati) questo può rompersi perché si desidera eseguire azioni sui dati salvati prima che vengano salvati. Ho scritto questo piccolo aiuto

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);
}

Usato come

var existingBooking = await GetWithRetry<Booking>(b => b.BookingKey == confirmCommand.BookingKey, "Booking key not found");

Qualche tranello? Task.Delay dovrebbe scalare bene dal momento che restituisce il thread al pool, e se i dati esistono nel DB la prima volta non dovrebbe dare un sovraccarico maggiore rispetto a un'attività extra wrapping?

Questa è la versione corrente del codice:

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;
}
    
posta Anders 29.09.2015 - 12:52
fonte

2 risposte

1

Se desideri solo i tentativi con timeout, puoi utilizzare un ciclo for anziché Stopwatch .

const int MaxWaitTime = 1000; //ms
const int SleepTime = 100; //ms

public async Task<TEntity> GetWithRetry<TEntity>(Expression<Func<TEntity, bool>> predicate)
    where TEntity : class
{
    TEntity entity = null;
    // run while under wait time and entity is null
    for (var i = 0; i < MaxWaitTime && entity == null; i += SleepTime) 
    {
        // only delay after first attempt
        if (i != 0) await Task.Delay(SleepTime);

        entity = await _context.DbSet<TEntity>().FirstOrDefaultAsync(predicate);
    }
    // I'd let the caller decide whether to throw on null here
    return entity;
}

Ma

Sembra che una soluzione migliore sarebbe quella di aggiungere una chiamata per i tuoi clienti che si completa solo quando i dati sono scritti. Capire che il blocco per l'IO è un problema di prestazioni, non farlo! Prova a restituire un Task , magari con TaskCompletionSource<T> . Quindi è possibile eseguire l'operazione in modo asincrono senza bloccare e fare riattivare il server per completare la chiamata quando viene completato Task .

Esempio di scenario di raggruppamento:

// api method
public async Task DoSomethingAsync(...)
{
    // using bool as dummy type, not really returning result
    var notifySource = new TaskCompletionSource<bool>();

    // pass into your data infrastructure
    // returns without performing save
    MyDatabase.AddToWriteBatch(..., notifySource);

    // wait for the task to complete
    await notifySource.Task;
}

Quando l'infrastruttura di dati si mette in moto per salvare i dati, avvisa i chiamanti di completamento:

public void Process(Batch batch) {
    ...
    // call to database
    Database.BatchWrite(batch.DataCollection);
    foreach (var taskSource in batch.TaskSources)
        taskSource.SetResult(true); // this will trigger Task completion
}

In questo modo, non stai ancora bloccando l'IO nella chiamata API. Ma puoi offrire al cliente un modo per essere sicuro che la loro scrittura venga confermata prima di eseguire altre operazioni.

o

Potresti usare TaskCompletionSource<TEntity> per le letture e posizionare le letture nella stessa coda con le scritture, in modo che le letture siano garantite per essere eseguite dopo le scritture.

    
risposta data 30.09.2015 - 22:58
fonte
0

Nella mia esperienza, tentativi come questa sono una cattiva idea.

Ragionamento:

  • Ti rendono il codice intrinsecamente non deterministico. Quante volte colpisci il db? se hai il null back hai solo bisogno di aspettare più a lungo o c'è un problema?

  • Introducono un problema di prestazioni. Se si dispone di un processo che chiama questa funzione, l'operazione richiede potenzialmente un timeout di millisecondi.

  • Problemi di threading. se stai eseguendo più operazioni, ad esempio richieste web, ognuna verrà generata su questi loop. se il timeout richiede più tempo della frequenza delle richieste, tutto si fermerà.

Se vuoi fare qualcosa una volta e l'evento si è verificato, dovresti avere un evento che attiva il processo.

puoi farlo avendo una coda di compiti e un processo di lavoro, o se è tutto nello stesso codice, semplicemente. Continuato con (NextStep)

    
risposta data 01.10.2015 - 17:36
fonte

Leggi altre domande sui tag