Redis + ActionController :: I thread live non stanno morendo

Background: abbiamo creato una funzione di chat in una delle nostre applicazioni Rails esistenti. Stiamo utilizzando il nuovo modulo ActionController::Live ed eseguiamo Puma (con Nginx in produzione) e ci iscriviamo ai messaggi tramite Redis. Stiamo utilizzando il lato client EventSource per stabilire la connessione in modo asincrono.

Riepilogo dei problemi: i thread non muoiono mai quando la connessione viene interrotta.

Ad esempio, se l’utente dovesse allontanarsi, chiudere il browser o persino accedere a una pagina diversa all’interno dell’applicazione, verrà generato un nuovo thread (come previsto), ma il vecchio continuerà a essere pubblicato.

Il problema, come attualmente vedo, è che quando si verifica una di queste situazioni, il server non ha modo di sapere se la connessione sul terminale del browser è terminata, fino a quando qualcosa tenta di scrivere su questo stream interrotto, cosa che non succederebbe mai una volta che il browser si è allontanato dalla pagina originale.

Questo problema sembra essere documentato su github , e domande simili sono state poste su StackOverflow qui (piuttosto esattamente la stessa domanda) e qui (riguardo al numero di thread attivi) .

L’unica soluzione che sono riuscito a trovare, in base a questi post, è implementare un tipo di thread / connessione poker. Il tentativo di scrivere su una connessione interrotta genera un IOError che posso catturare e chiudere correttamente la connessione, permettendo al thread di morire. Questo è il codice del controller per quella soluzione:

 def events response.headers["Content-Type"] = "text/event-stream" stream_error = false; # used by flusher thread to determine when to stop redis = Redis.new # Subscribe to our events redis.subscribe("message.create", "message.user_list_update") do |on| on.message do |event, data| # when message is received, write to stream response.stream.write("messageType: '#{event}', data: #{data}\n\n") end # This is the monitor / connection poker thread # Periodically poke the connection by attempting to write to the stream flusher_thread = Thread.new do while !stream_error $redis.publish "message.create", "flusher_test" sleep 2.seconds end end end rescue IOError logger.info "Stream closed" stream_error = true; ensure logger.info "Events action is quitting redis and closing stream!" redis.quit response.stream.close end 

(Nota: il metodo degli events sembra essere bloccato nel richiamo del metodo subscribe . Tutto il resto (lo streaming) funziona correttamente quindi presumo che sia normale).

(Un’altra nota: il concetto di thread flusher ha più senso come un singolo processo in background di lunga durata, un po ‘come un raccoglitore di thread di garbage.Il problema con la mia implementazione sopra è che viene generato un nuovo thread per ogni connessione, che è inutile. tentare di implementare questo concetto dovrebbe farlo più come un singolo processo, non tanto quanto ho delineato. Aggiornerò questo post quando rieseguirò l’implementazione come singolo processo in background).

Il lato negativo di questa soluzione è che abbiamo solo ritardato o attenuato il problema, non completamente risolto. Abbiamo ancora 2 thread per utente, oltre ad altre richieste come ajax, che sembra terribile da una prospettiva di ridimensionamento; sembra completamente irraggiungibile e poco pratico per un sistema più grande con molte possibili connessioni simultanee.

Mi sento come se mi mancasse qualcosa di vitale; Trovo piuttosto difficile credere che Rails abbia una caratteristica che è così evidentemente rotta senza implementare un correttore di connessione personalizzato come ho fatto io.

Domanda: Come possiamo permettere alle connessioni / thread di morire senza implementare qualcosa di banale come un ‘connection poker’ o un garbage collector?

Come sempre fammi sapere se ho lasciato qualcosa.

Aggiornamento Solo per aggiungere un po ‘di informazioni extra: Huetsch su github ha postato questo commento sottolineando che SSE è basato su TCP, che normalmente invia un pacchetto FIN quando la connessione è chiusa, lasciando che l’altra estremità (il server in questo caso) sappia che è sicuro chiudere la connessione. Huetsch sottolinea che o il browser non sta inviando quel pacchetto (forse un bug nella libreria EventSource ?), O Rails non lo sta catturando o facendo qualcosa (sicuramente un bug in Rails, se è così). La ricerca continua …

Un altro aggiornamento usando Wireshark, posso davvero vedere i pacchetti FIN inviati. Devo ammettere che non sono molto esperto o esperto di contenuti a livello di protocollo, tuttavia, da quello che posso dire, rilevo definitivamente un pacchetto FIN che viene inviato dal browser quando stabilisco la connessione SSE usando EventSource dal browser, e nessun pacchetto inviato se io rimuovere quella connessione (che significa no SSE). Anche se non sono terribilmente in gamba sulla mia conoscenza di TCP, questo sembra indicarmi che la connessione è effettivamente terminata correttamente dal client; forse questo indica un bug in Puma o Rails.

Un altro aggiornamento @JamesBoutcher / boutcheratwest (github) mi ha indirizzato a una discussione sul sito web redis riguardante questo problema, in particolare per quanto riguarda il fatto che il metodo di .(p)subscribe non si arresta mai. Il poster su quel sito ha messo in evidenza la stessa cosa che abbiamo scoperto qui, che l’ambiente Rails non viene mai informato quando la connessione lato client è chiusa, e quindi non è in grado di eseguire il metodo di .(p)unsubscribe dell’iscrizione. Si interroga su un timeout per il metodo di .(p)subscribe , che penso possa funzionare anche se non sono sicuro di quale metodo (la connessione che ho descritto sopra, o il suo suggerimento di timeout) sarebbe una soluzione migliore . Idealmente, per la soluzione di connessione al poker, mi piacerebbe trovare un modo per determinare se la connessione è chiusa dall’altra parte senza scrivere nello stream. Come è ora, come puoi vedere, devo implementare il codice lato client per gestire separatamente il mio messaggio “poking”, che credo sia invadente e sciocco come diamine.