Forum: Italian Ruby user group Ajax, Observers e autorefresh

Posted by Luca G. Soave (lgs)
on 2010-03-10 10:51
Ciao a tutti, mi servirebbe una dritta su come implementare
l'autorefresh di una lista di posts.

L'idea e', che se un utente sta guardando una pagina che lista i suoi
post e quelli del suo gruppo, dovrebbe vedere arrivare i nuovi post
eseguiti dagli altri in tempo reale, cioe dinamicamente senza fare il
reload della pagina.

Non ho molta dimestichezza con Ajax, ma dovrebbe avere a che fare con i
metodi di ActionView::Helpers::PrototypeHelper:

    * build_callbacks
    * build_observer
    * button_to_remote
    * evaluate_remote_response
    * form_remote_for
    * form_remote_tag
    * link_to_remote
    * method_option_to_s
    * observe_field
    * observe_form
    * options_for_ajax
    * periodically_call_remote
    * remote_form_for
    * remote_function
    * submit_to_remote
    * update_page
    * update_page_tag

la pagina che devo refreshare e' l'index dell'home controller:

--------------------------------------------------------------
root@webby2066:/var/rails/flitter# cat  ./app/views/home/index.html.erb
...
<%= render :partial => "flits_list", :locals => { :flits => @flits }%>
--------------------------------------------------------------

che sostanzialmente chiama un parziale che implementa ogni singolo post:

--------------------------------------------------------------
root@webby2066:/var/rails/flitter# cat
./app/views/home/_flits_list.html.erb
<ul id="flits_list">
  <% flits.each do |flit| %>
    <li<% if flits.first == flit %> class="first"<% end %>>
      <%= image_tag flit.user.gravatar_url %>
      <div class="flit_message_container">
        <%= link_to flit.user.username,
user_flits_path(flit.user.username) %>
        <%= h flit.message %>
        <div class="time_ago">
          <%= distance_of_time_in_words_to_now(flit.created_at) %> ago
        </div>
      </div>
      <div class="clear"></div>
    </li>
  <% end %>
</ul>
--------------------------------------------------------------

in un controller diverso da home,  ho un "evento trigger" che e'
flit.save!  e dovrebbe scatenare il refresh di home/index.html.erb

Non mi e' del tutto chiaro pero' se c'e' un metodo tra quelli elencati
prima, idoneo a rilevare l'evento trigger flit.save! che in sostanza
salva un nuovo post nel DB.

Ringrazio in anticipo per ogni suggerimento
Ciao Luca
Posted by Pietro Giorgianni (giorgian)
on 2010-03-10 11:17
(Received via mailing list)
Il 10 marzo 2010 10.51, Luca G. Soave <luca.soave@gmail.com> ha scritto:
> metodi di ActionView::Helpers::PrototypeHelper:
>    * observe_form
>    * options_for_ajax
>    * periodically_call_remote
>    * remote_form_for
>    * remote_function
>    * submit_to_remote
>    * update_page
>    * update_page_tag

Ciao,

premetto che, dopo un periodo iniziale irto di difficoltà, ho
abbandonato del tutto rjs e i PrototypeHelper (e veramente anche
Prototype, perché scoprire jQuery è stato un po' come quando cerchi di
lavorare a tentoni nel buio e a un certo punto accendono la luce).

Quello che ti serve è periodically_call_remote; la cosa più pulita è 
avere un metodo di un qualche controller che ti restituisca un json
contenente una variabile boolean che ti dica se è necessario il
refresh oppure no, in modo che la risposta sia sempre rapidissima.

Poi, quando ti serve fare il refresh, potresti fare una nuova chiamata
all'index di home (ad esempio con remote_function), da cui farti
restituire solo il partial da rimpiazzare (la funzione potrebbe anche
usare rjs, volendo...).


La questione è se vuoi fare tutto con gli helper di rails o se vuoi
fare tutto con javascript.


pietro
Posted by Alessandro Scolavino (ninjinka)
on 2010-03-10 13:01
puoi anche farti spedire direttamente i nuovi post in html e aggiungerli 
alla lista oppure ricaricare semplicemente la lista:

new Ajax.PeriodicalUpdater('items', '/items', {
  method: 'get', frequency: 3, decay: 2
});



> Poi, quando ti serve fare il refresh, potresti fare una nuova chiamata
> all'index di home (ad esempio con remote_function), da cui farti
> restituire solo il partial da rimpiazzare (la funzione potrebbe anche
> usare rjs, volendo...).
Posted by Pietro Giorgianni (giorgian)
on 2010-03-10 13:28
(Received via mailing list)
Il 10 marzo 2010 13.01, Alessandro Scolavino <scolas@gmail.com> ha 
scritto:
> puoi anche farti spedire direttamente i nuovi post in html e aggiungerli
> alla lista oppure ricaricare semplicemente la lista:
>
> new Ajax.PeriodicalUpdater('items', '/items', {
>  method: 'get', frequency: 3, decay: 2
> });
>

Sì, è anche questa una soluzione ma, avendola provata in passato, non
fa un bell'effetto, specie se la lista non è leggerissima.

Magari prova, in effetti così è molto semplice, ma se vedi che lo
sfarfallìo è percettibile, riconsidera l'idea di modificare il dom
solo quando serve.


pietro
Posted by Luca G. Soave (lgs)
on 2010-03-10 17:04
Grazie a Pietro e Alessandro x le info.

Mi pare di capire che sia periodically_call_remote che 
Ajax.PeriodicalUpdater siano metodi "attivi" di polling, per cosi' dire.

Non esiste, che voi sappiate, un Observer di ActiveRecord in grado di 
mandare una callback per una data commit sul DB (.save)? Oppure una cosa 
tipo after_create() di ActiveRecord::Callbacks che scatena un evento 
ajax?

Sto solo immaginando, perche' non ho esperienza in questi metodi.

Il problema e' che dentro il partial, 
./app/views/home/_flits_list.html.erb c'e diversa roba, tra qui una 
query al db <%= h flit.message %> che verrebbe eseguita nel mio caso 
ogni 2 secondi x tutto il periodo in cui il browser utente rimane aperto 
sulla pagina, moltiplicato x tutti gli utenti collegati nell'unita' di 
tempo ... poco scalabile credo.

Sto solo speculando, ma mi piacerebbe innescare un certo numero di 
soluzioni percorribili e di confronti.

Grazie a tutti
Luca G.S.
Posted by Andrea Pavoni (apeacox)
on 2010-03-10 18:04
(Received via mailing list)
On 10/03/2010 17:04, Luca G. Soave wrote:
>    
in teoria, non ha molto senso mettere mettere una logica che implica
AJAX dentro un modello, altrimenti non sarebbe un pattern MVC :P

in caso puoi farlo nel controller: riprendendo l'esempio di after_create
di ActiveRecord, vorrà dire che nella action 'create' farai qualcosa
dopo aver salvato il modello.

per quanto riguarda il partial, puoi usare una chiamata AJAX
periodicamente, come ti hanno già consigliato. per quanto riguarda la
scalabilità, di sicuro puoi usare il "fragment caching". Dovrai
impostare un observer sul modello per aggiornare il contenuto del
partial *cachato*.

se poi la scalabilità è davvero un problema urgente, potresti valutare
qualcosa tipo Redis (un db chiave/valore che fa anche molte altre cose:
http://code.google.com/p/redis/) + redis_store (plugin Rails per fare il
caching usando Redis: http://github.com/jodosha/redis-store). Tra le
altre cose, entrambe i progetti sono scritti da programmatori italiani 
:-)

ciao,
A.
Posted by Marco Mastrodonato (marcomd)
on 2010-03-10 18:35
Luca G. Soave wrote:
> Grazie a Pietro e Alessandro x le info.
> 
> Mi pare di capire che sia periodically_call_remote che 
> Ajax.PeriodicalUpdater siano metodi "attivi" di polling, per cosi' dire.
> 
> Non esiste, che voi sappiate, un Observer di ActiveRecord in grado di 
> mandare una callback per una data commit sul DB (.save)? Oppure una cosa 
> tipo after_create() di ActiveRecord::Callbacks che scatena un evento 
> ajax?

Direi che si tratta di polling, un aggiornamento sul server come può 
avere relazioni con i clients che interrogano il database? Impostando un 
intervallo di check moderato, non credo si appesantisca troppo il server
Posted by Pietro Giorgianni (giorgian)
on 2010-03-11 00:39
(Received via mailing list)
Il 10 marzo 2010 17.04, Luca G. Soave <luca.soave@gmail.com> ha scritto:
> Grazie a Pietro e Alessandro x le info.
>
> Mi pare di capire che sia periodically_call_remote che
> Ajax.PeriodicalUpdater siano metodi "attivi" di polling, per cosi' dire.
>
> Non esiste, che voi sappiate, un Observer di ActiveRecord in grado di
> mandare una callback per una data commit sul DB (.save)? Oppure una cosa
> tipo after_create() di ActiveRecord::Callbacks che scatena un evento
> ajax?

Come ti hanno già detto, no, non si può. Alcune applicazioni web
implementano eventi push tenendo aperto un socket (java o flash) ma, a
parte il fatto di avere una dipendenza in più, non credere che, per il
server, mantenere (potenzialmente) migliaia di connessioni aperte sia
un carico trascurabile.
Altre applicazioni simulano un evento push "trattenendo" le risposte a
eventi poll, ma è una cosa ancora più complicata e pesante per il
server.

La cosa più semplice e leggera è appunto quella suggeritati (vedi sotto).

> Sto solo immaginando, perche' non ho esperienza in questi metodi.
>
> Il problema e' che dentro il partial,
> ./app/views/home/_flits_list.html.erb c'e diversa roba, tra qui una
> query al db <%= h flit.message %> che verrebbe eseguita nel mio caso
> ogni 2 secondi x tutto il periodo in cui il browser utente rimane aperto
> sulla pagina, moltiplicato x tutti gli utenti collegati nell'unita' di
> tempo ... poco scalabile credo.

Ecco, qui c'è un problema grossissimo, indipendentemente da ajax e
quant'altro: le view *non devono* fare query. Le view devono solo
mostrare, nient'altro.

Poi, c'è un altro problema, e cioè che il partial è complesso. E anche
stavolta, questo è un problema indipendentemente da ajax.
Semplicemente, scomponi il partial in pezzi più piccoli, e uno di
questi pezzi si limiterà a mostrare la lista.

> una query che verrebbe eseguita nel mio caso ogni 2 secondi

Due secondi mi sembra un'esagerazione, in genere cinque (o anche
dieci) secondi vanno più che bene; sei proprio sicuro che sia
indispensabile? Molte applicazioni mostrano una buona reattività pur
tenendosi appunto intorno ai dieci secondi e oltre.

Se ripetere tante volte la stessa query è davvero un problema, ci sono
tecnologie ad hoc per la persistenza dello stato (che in questo caso è 
nuovi post sì / nuovi post no), come ti ha suggerito Andrea, che però 
ha senso indagare solo se è vera almeno una delle due ipotesi:

a) hai già centinaia di utenti prenotati che terranno aperto il
browser sulla tua applicazione (ma questo probabilmente significa che
hai bisogno di una certa infrastruttura, ad esempio una banda "seria"
e un server come si deve);

b) hai voglia di imparare a usare redis :)

Tra l'altro, come dicevo nell'email precedente, usare una chiamata
ajax che ridisegni migliaia di volte la stessa identica lista è uno
spreco enorme, ed è un carico sia per il server (che finché non
finisce di trasmettere una risposta resta impegnato, ed ha così minore
capacità di risposta per gli altri) che per il client, e l'utente si
ritroverebbe un computer inchiodato da un browser che ridisegna lo
stesso pezzo di pagina; anche l'effetto visivo non è il massimo,
perché lo sfarfallìo si vede.

Ma questo non è necessario: quando mostri la pagina passi al
javascript l'ora corrente (lato server), così il javascript può 
interrogare il server chiedendo: è cambiato niente dall'ora X? Se la
risposta è no, non fai niente; se è sì, si ridisegna la pagina.


pietro
Posted by Luca G. Soave (lgs)
on 2010-03-11 11:01
Ok Pietro, grazie per i numerosi consigli e le opzioni che mi hai 
prospettato. Mi sembra di avere un quadro un po piu' completo sullo 
"stato dell'arte".

Provo a distillare il mio compromesso sugli elementi forniti.

Ciao
Luca
Please log in before posting. Registration is free and takes only a minute.
Existing account (Switch to SSL-encrypted connection)
NEW: Do you have a Google/GoogleMail or Yahoo account? No registration required!
Log in with Google account | Log in with Yahoo account
No account? Register here.