5

Sto utilizzando StackExchange.Redis (SE.R d'ora in poi) nella mia applicazione Nancy. C'è un singolo globale ConnectionMultiplexer che viene automaticamente trasmesso da Nancy's TinyIoC tramite parametri del costruttore, e ogni volta che provo e uso GetDatabase e uno dei metodi *Async (i metodi di sincronizzazione iniziano solo a fallire dopo che uno dei metodi asincroni sono stati tentati) la mia applicazione deadlock.StackExchange.Redis Deadlocking

Guardando il mio pile parallele sembra che io ho quattro thread:

  1. Il thread che ha chiamato Result su uno dei miei compiti che utilizza SE.R. (C'è molta roba di Nancy in pila, poi una chiamata alla mia biblioteca che utilizza SE.R e una chiamata a Result. La parte superiore della pila è Monitor.Wait).
  2. Un thread che ha generato altri due thread. Presumo che questo sia gestito da SE.R. Inizia con Native to Managed Transition, ThreadHelper.ThreadStart e nella parte superiore della pila è ThreadHelper.ThreadStart_Context.
  3. Una piccola pila che è bloccato in questo modo:
    • Monitor.Wait
    • Monitor.Wait
    • SocketManager.WriteAllQueues
    • SocketManager.cctor.AnonymousMethod__16
  4. Un'altra piccola pila che assomiglia a questo:
    • Managed to Native Transition
    • SocketManager.ReadImpl
    • SocketManager.Read
    • SocketManager.cctor.AnonymousMethod__19

Sono quasi sicuro che questo è una situazione di stallo di qualche tipo. Penso anche che potrebbe avere qualcosa a che fare con this question. Ma non ho idea di cosa fare al riguardo.

Il ConnectionMultiplexer è istituito in un Nancy IRegistrations con il seguente codice:

var configOpts = new ConfigurationOptions { 
    EndPoints = { 
    RedisHost, 
    }, 
    Password = RedisPass, 
    AllowAdmin = false, 
    ClientName = ApplicationName, 
    ConnectTimeout = 10000, 
    SyncTimeout = 5000, 
}; 
var mux = ConnectionMultiplexer.Connect(configOpts); 
yield return new InstanceRegistration(typeof (ConnectionMultiplexer), mux); 

mux è l'istanza che viene condivisa da tutto il codice che ne facciano richiesta nella loro lista parametro del costruttore.

Ho una classe chiamata SchemaCache. Un piccolo pezzo di esso (che include il codice che genera l'errore in questione) segue:

public SchemaCache(ConnectionMultiplexer connectionMultiplexer) { 
    ConnectionMultiplexer = connectionMultiplexer; 
} 

private ConnectionMultiplexer ConnectionMultiplexer { get; set; } 

private async Task<string[]> Cached(string key, bool forceFetch, Func<string[]> fetch) { 
    var db = ConnectionMultiplexer.GetDatabase(); 

    return forceFetch || !await db.KeyExistsAsync(key) 
     ? await CacheSetSet(db, key, await Task.Run(fetch)) 
     : await CacheGetSet(db, key); 
} 

private static async Task<string[]> CacheSetSet(IDatabaseAsync db, string key, string[] values) { 
    await db.KeyDeleteAsync(key); 
    await db.SetAddAsync(key, EmptyCacheSentinel); 

    var keysSaved = values 
     .Append(EmptyCacheSentinel) 
     .Select(val => db.SetAddAsync(key, val)) 
     .ToArray() 
     .Append(db.KeyExpireAsync(key, TimeSpan.FromDays(1))); 
    await Task.WhenAll(keysSaved); 

    return values; 
} 

private static async Task<string[]> CacheGetSet(IDatabaseAsync db, string key) { 
    var results = await db.SetMembersAsync(key); 
    return results.Select(rv => (string) rv).Without(EmptyCacheSentinel).ToArray(); 
} 

// There are a bunch of these public methods: 
public async Task<IEnumerable<string>> UseCache1(bool forceFetch = false) { 
    return await Cached("the_key_i_want", forceFetch,() => { 
     using (var cnn = MakeConnectionToDatabase("server", "databaseName")) { 
      // Uses Dapper: 
      return cnn.Query<string>("--expensive sql query").ToArray(); 
     } 
    }); 
} 

Ho anche una classe che fa uso di questo in un metodo che richiede alcune delle informazioni dalla cache:

public OtherClass(SchemaCache cache) { 
    Cache = cache; 
} 

private SchemaCache Cache { get; set; } 

public Result GetResult(Parameter parameter) { 
    return Cache.UseCache1().Result 
     .Where(r => Cache.UseCache2(r).Result.Contains(parameter)) 
     .Select(r => CheckResult(r)) 
     .FirstOrDefault(x => x != null); 
} 

Tutto di quanto sopra funziona bene in LinqPad dove c'è solo una istanza di tutto in questione. Invece fallisce con uno TimeoutException (e successivamente un'eccezione su nessuna connessione disponibile).L'unica differenza è che ottengo un'istanza della cache tramite dependency injection, e sono abbastanza sicuro che Nancy usi Tasks per parallelizzare le richieste.

+0

Mostraci il tuo codice. –

+0

Ancora cercando di riassumere. – Crisfole

+0

Stai per caso utilizzando una transazione o un lotto? C'è un modo molto semplice per bloccarti lì (che è altrettanto facilmente risolto) –

risposta

10

Qui andiamo:

return Cache.UseCache1().Result 

braccio; deadlock. Ma niente a che vedere con StackExchange.Redis.

Almeno, dalla maggior parte dei provider di contesto di sincronizzazione. Questo perché i tuoi await richiedono tutte l'attivazione del contesto di sincronizzazione, che può significare "sul thread dell'interfaccia utente" (winforms, WPF) o "sul thread di lavoro attualmente designato" (WCF, ASP.NET, MVC, ecc.) . Il problema qui è che questa discussione non sarà mai disponibile per elaborare quegli articoli, perché .Result è una chiamata sincrona e bloccante. Quindi nessuno dei callback di completamento verrà elaborato, perché l'unico thread che può processarli è in attesa che finiscano prima di rendersi disponibili.

Nota: StackExchange.Redis non utilizza il contesto di sincronizzazione; si disconnette esplicitamente dal contesto di sincronizzazione per evitare di essere la causa di deadlock (questo è normale e raccomandato per le librerie). Il punto chiave è che il tuo codice non è.

Opzioni:

  • non mescolare async e .Result/.Wait(), o
  • hanno tutte le await chiamate (o almeno quelli sotto .UseCache1()) chiamare esplicitamente .ConfigureAwait(false) - Si noti, tuttavia, che questo significa che i completamenti non si verificano nel contesto di chiamata!

La prima opzione è la più semplice; se è possibile isolare un albero di chiamate che non dipendono dal contesto di sincronizzazione, allora il secondo approccio è fattibile.

Questo è un problema molto comune; I did pretty much the same thing.

+0

You rock. Sono anche contento che tu abbia fatto la stessa cosa. Grazie mille per averlo spiegato così bene. – Crisfole

+0

Il mio stack è fondamentalmente: NancyFx + Marc Gravell ... quindi è sempre bello avere aiuto dalla fonte;) – Crisfole