Backend, Frontend

Sidekiq i Pusher – dlaczego warto używać ich razem?

Twoja aplikacja ma problemy z przekroczeniem czasu oczekiwania na wykonanie zapytania lub niektóre żądania, takie jak generowanie danych, zabierają zbyt dużo czasu? A może chcesz połączyć wykonywanie jobów z Sidekiq’a z częścią front-endową?

Piotr Jaworski. Ruby on Rails & JavaScipt Developer, z zamiłowania tworzy techniczne teksty dla krakowskiego software house Nopio. Obecnie pracuje w Londynie. Programowanie to nie tylko jego sposób na życie, ale przede wszystkim pasja. Uwielbia podróżować i aktywnie spędzać czas. Fan sportu, głównie siatkówki i futbolu.


Możesz na przykład powiadomić użytkownika, że dane, o które prosił, są gotowe do pobrania lub wyświetlenia. Spowoduje to wysłanie zwykłego powiadomienia, takiego jak na Facebooku, gdy ktoś doda Cię do znajomych lub napisze nową wiadomość. Jeśli odpowiedź brzmi „tak”, ten tutorial jest dla Ciebie!

Wstęp

W tym tutorialu omówię jak działają Sidekiq i Pusher, jak połączyć je na back-endzie i front-endzie oraz wyjaśnię korzyści z ich wspólnego wykorzystania. Zaczynajmy!

Myślę, że prawdopodobnie wiesz coś o Sidekiq lub nawet Pusher — jeśli nie, tutaj jest krótkie wprowadzenie dla ciebie.

Sidekiq to prosty procesor pracy w tle napisany w języku Ruby. Jest znacznie bardziej wydajny niż Resque lub DelayedJob. W jaki sposób działa? To narzędzie pozwala aplikacji na przetwarzanie kodu w tle w Ruby, bez konfliktów z przychodzącymi żądaniami do serwera. Możesz przetwarzać ogromne zapytania SQL, generować plik, przesyłać go później do S3 itd. — bez żadnych ograniczeń!

Pusher to narzędzie używane do budowania aplikacji działających w czasie rzeczywistym, używany do takich funkcjonalności, jak czat lub pasek postępu przesyłania plików. Możesz zaimplementować kod na back-endzie, przetworzyć go, a następnie wysłać wynik do front-endu za pomocą web-sockets. To właśnie robi Pusher!

Jak możemy je połączyć? To naprawdę proste, musimy wysłać / przesłać wynik przez websocket z naszego back-endu do konkretnego kanału i nazwy połączenia do front-endu. Zasadniczo front-end czeka na dane na konkretnym kanale i połączeniu. Po otrzymaniu danych robi coś z wynikiem — np. wyświetla go.

Przykłady wykorzystania? Czat, wideokonferencja, system powiadomień, transmisja danych na żywo lub jeszcze więcej! Wszystkie te funkcje można zbudować za pomocą Pushera. Świetne tutoriale na ten temat można znaleźć tutaj.

Wstęp do aplikacji

Skoro teraz wiesz więcej na temat obu narzędzi, porozmawiajmy o aplikacji, którą będziemy budować. Zrobimy prosty szkielet i połączymy Sidekiq i Pusher. Zasadniczo poradzimy sobie z ciężkim przetwarzaniem danych w Sidekiq i wyślemy wynik na front-end — w czasie rzeczywistym. Chcę tylko pokazać koncepcję aplikacji, opisując, jak możesz stworzyć coś większego i dostosowanego do Twoich potrzeb.

Stwórzmy nową aplikację:

Shell
1     $ rails new sidekiq_with_pusher

Następnie zrobimy trochę porządków w Gemfile. Pozostawimy tylko potrzebne gemy:

Ruby
1     source 'https://rubygems.org'
2
3     git_source(:github) do |repo_name|
4       repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/")
5       "https://github.com/#{repo_name}.git"
6     end
7
8     gem 'rails', '~> 5.1.4'
9     gem 'sqlite3'
10    gem 'puma', '~> 3.7'
11    gem 'sass-rails', '~> 5.0'
12    gem 'uglifier', '>= 1.3.0'
13    gem 'redis', '~> 3.0'
14
15    group :development, :test do
16      gem 'pry-rails'
17    end
18
19    group :development do
20      gem 'listen', '>= 3.0.5', '< 3.2'
21      gem 'spring'
22      gem 'spring-watcher-listen', '~> 2.0.0'
23    end

 
git_source(:github) do |repo_name|
  repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/")
  "https://github.com/#{repo_name}.git"
end
 
gem 'rails', '~> 5.1.4'
gem 'sqlite3'
gem 'puma', '~> 3.7'
gem 'sass-rails', '~> 5.0'
gem 'uglifier', '>= 1.3.0'
gem 'redis', '~> 3.0'
 
group :development, :test do
  gem 'pry-rails'
end
 
group :development do
  gem 'listen', '>= 3.0.5', '< 3.2'
  gem 'spring'
  gem 'spring-watcher-listen', '~> 2.0.0'
end

Konfiguracja aplikacji

Dodajmy potrzebne gemy do Gemfile.

Ruby
1    gem 'sidekiq'
2    gem 'foreman'
3    gem 'jquery-rails'

Jak zapewne wiesz, jQuery nie jest już dodane w Railsach, więc musimy dodać je ręcznie. Będziemy używać jego do robienia zapytań AJAXowych bez pisania zbyt dużej ilości kodu.
Dodaliśmy także Sidekiq i Foreman.

Czym jest Foreman? To gem, który zarządza plikami Procfile wykorzystywanych na Heroku, gdzie możemy zdefiniować jakie serwisy będą wykonywane w naszej aplikacji.

Jeśli nie masz zainstalowanego Redisa, zrób to proszę teraz. Będziemy potrzebować go do włączenia Sidekiqa — w innym przypadku nie będzie działał:

Shell
1    $ brew install redis

Jeśli nie używasz systemu macOS, tutaj znajdziesz tutorial, który opisuje sposób instalacji Ubuntu.

Następnie dodaj Procfile, który uruchomi w jednej karcie terminala dwa procesy — redis-server i Sidekiq

Ruby
1    redis: redis-server /usr/local/etc/redis.conf
2    worker: bundle exec sidekiq

Zainstalujmy wszystkie dodane gemy:

Shell
1    $ bundle install

Teraz uruchomimy nasz Procfile:

Shell
1    $ foreman start -f Procfile

Nasza aplikacja musi mieć jakiś kontroler i akcję, wygenerujemy więc HomeController. Stwórzmy także akcję index, która będzie zajmowała się renderowaniem plików oraz akcję generate, która będzie zbierała wszystkie potrzebne nam dane:

Shell
1    $ rails g controller Home index generate

Następnie stwórzmy workera, który będzie wszystko razem spinał:

Shell
1    $ rails g sidekiq:worker Generator

Zaktualizuj proszę ścieżki, by oznaczyć stronę główną i akcję generate:

Ruby
1    Rails.application.routes.draw do
2    root 'home#index'
3    get 'home/generate'
4    end
5    Sign up for free

Nie potrzebujemy Turbolinków, więc usuńmy je z application.html.erb:

XHTML
1    <!DOCTYPE html>
2    <html>
3       <head>
4         <title>SidekiqWithPusher</title>
5         <%= csrf_meta_tags %>
6
7         <%= stylesheet_link_tag 'application', media: 'all' %>
8         <%= javascript_include_tag 'application' %>
9       </head>
10
11      <body>
12        <%= yield %>
13      </body>
14   </html>



</body>
</html>

Usuń je z pliku application.js, a także dodaj bibliotekę jQuery:

JavaScript
1    //= require rails-ujs
2    //= require jquery
3    //= require jquery_ujs
4    //= require_tree .

Teraz spróbuj zrestartować serwer, żeby sprawdzić czy wszystko zostało poprawnie skonfigurowane.

Shell
1    $ rails server

Wejdź na stronę http://localhost:3000.Twoja aplikacja ma problemy z przekroczeniem czasu oczekiwania na wykonanie zapytania lub niektóre żądania, takie jak generowanie danych, zabierają zbyt dużo czasu? A może chcesz połączyć wykonywanie jobów z Sidekiq’a z częścią front-endową?

Główny logika aplikacji – serwis

Kolejnym zadaniem jest stworzenie logiki naszej aplikacji — serwisu, który będzie odpowiadał za gromadzenie danych lub robienie czegokolwiek, co sobie wymarzysz. Stworzymy przykład i udajmy, że te ogromne operacje trwają bardzo długo poprzez wykorzystanie funkcji sleep na 30 sekund. Udawajmy, że generujemy plik pdf, ładujemy go na serwer i otrzymujemy link url.

Stwórzmy plik file_generator.rb pod adresem app/services:

Ruby
1    class FileGenerator
2       def call
3         process_data
4         generate_file_url
5       end
6
7       private
8
9       def process_data
10        # imagine heavy sql queries, data processing
11        sleep(30)
12       end
13
14       def generate_file_url
15         'http://example.com/file.pdf'
16       end
17     end

Teraz zaktualizujmy i wygenerujmy metodę w home_controller.rb w celu wywołania naszego serwisu i otrzymania kodu statusu 200:

Ruby
1    def generate
2          FileGenerator.new.call
3          head :ok
4       end

Zaktualizuj proszę plik index.html.erb i dodaj przycisk wywołujący wygenerowaną akcję.

XHTML
1    <%= button_to "Generate", home_generate_path, id: 'generate_data' %>
2
3    <div id='result'></div>

Wywołamy to poprzez zapytanie AJAX. Przed wykonaniem procesu sprawdzimy czy zapytanie jest przetwarzane i kiedy to zostanie wykonane, wyrenderujemy url do pliku PDF. Dodajmy to do application.js:

JavaScript
1    ...
2
3    $(document).on('ready', function() {
4      $('#generate_data').click(function(event) {
5        event.preventDefault();
6        $.ajax({
7          method: 'GET',
8          url: '/home/generate',
9          beforeSend: function() {
10           $('#result').html('Processing...');
11         },
12         success: function(data) {
13           $('#result').html('Done!');
14         }
15       });
16    });
17 });

Po kliknięciu przycisku “Generuj” powinno zostać wysłane zapytanie.

Jak możesz się domyślać nasz serwer będzie bezczynny przez 30 sekund. No tak, musimy przecież zebrać dużo informacji i wgrać plik — to trochę zajmuje! A na koniec musimy wyświetlić url pliku! Jeśli używasz Heroku, Twój proces zostanie przerwany przez rack-timeout jeżeli trwa dłużej niż 30 sekund. Użytkownik przez to nigdy nie dostanie linku do pliku!

Nareszcie po 30 sekundowym oczekiwaniu dostajemy nasz url!

Nie wygląda to najlepiej, musimy dokonać refactoringu. Zrobimy to przy użyciu Pushera.

Sidekiq z Pusherem

Na początku artykułu opisałem czym jest Pusher. Teraz musimy tylko dodać go do naszej aplikacji. Zaktualizuj Gemfile, dodając bibliotekę potrzebną do zaimplementowania w Ruby — oraz po stronie klienta w JavaScriptcie. Będziemy przechowywać pewne wrażliwe informacje, więc dobrym pomysłem jest dodanie gemu Dotenv. Bądźcie jednak ostrożni — musi on być dodany tuż za gemem Rails, ponieważ ładuje on wszystkie zmienne środowiska.

Ruby
1    ..
2
3    gem 'rails', '~> 5.1.4'
4    gem 'dotenv-rails', '~> 2.2.1'
5
6    ...
7
8    gem 'pusher'
9    gem 'rails-assets-pusher', source: 'https://rails-assets.org'

W kolejnym kroku w celu instalacji wszystkiego uruchom bundlera:

Shell
1    $ bundle install

Jeśli w czasie instalacji pojawi się jakikolwiek problem usuń Gemfile.lock, a następnie uruchom ponownie bundle install.

Teraz musisz stworzyć profil w serwisie Pusher. W tym celu odwiedź stronę https://pusher.com/ i zaloguj się przy użyciu Github/Google lub stwórz nowe konto. Po zalogowaniu stwórz nową aplikację, front-end: JQuery / back-end: Ruby/Rails, pobierz klucze i dodaj je do pliku .env, np.:

PUSHER_APP_ID=11111
PUSHER_KEY=efwq3fewfsdf
PUSHER_SECRET=rewfwrgf

Skoro Pusher został zainstalowany i mamy swoje klucze, to możemy załadować je do naszej aplikacji. Stwórzmy więc initalizator pod config/initializers:

Ruby
1    require 'pusher'
2
3    Pusher.app_id = ENV.fetch('PUSHER_APP_ID')
4    Pusher.key = ENV.fetch('PUSHER_KEY')
5    Pusher.secret = ENV.fetch('PUSHER_SECRET')
6    Pusher.cluster = 'eu'
7    Pusher.logger = Rails.logger
8    Pusher.encrypted = true

Wspaniale, wszystko jest poprawnie skonfigurowane! Teraz nareszcie możemy zmienić logikę naszej aplikacji i napisać kawałek kodu wykorzystujący Pusher. Zaktualizujemy logikę FileGenerator.

Co chcemy tutaj osiągnąć? Mamy zamiar po prostu stworzyć joba Sidekiq’owego, które będzie działał w tle, a zapytanie POST będzie trwało 3 milisekundy, a nie 30 sekund.

Ruby
1    class FileGenerator
2      def call
3        GeneratorWorker.perform_async
4      end
5    end

Następnie naszym zadaniem jest implementacja logiki w GeneratorWorker. Co tu zrobimy? Będziemy przetwarzać całą logikę oraz przeprowadzać ciężkie i długo trwające operacje. Kolejną ważną rzeczą jest fakt, że musimy przepuścić wynik operacji – wygenerowany URL, do front-endu.

Jak to zrobimy? Musimy wywołać akcję publikacji na odpowiedni kanał. Czym jest kanał? Możemy zobrazować go sobie jako pokój, do którego się podłączamy. Jeśli to zrobimy, to możemy wykonywać różne akcje. Tak właściwie akcję możemy sobie wyobrazić jako pokój wewnątrz większego pokoju.

Stwórzmy więc kanał i nazwijmy go conversation-1 (może to być na przykład id z bazy danych) i stwórzmy akcję o nazwie send-message. Wszystkie te akcje będą dostępne dla klientów podłączonych do tego właśnie pokoju.

Dla każdej funkcjonalności powinieneś stworzyć oddzielny, unikatowy kanał dla każdego użytkownika, grupy użytkowników itd.

Co więcej Pusher oferuje coś takiego jak prywatne kanały do których dostęp mają tylko autoryzowani użytkownicy – np. Admini itp.

Teraz musimy dodać takie kawałek kodu – wysyłanie adresu URL do odpowiedniego kanału i akcji:

Ruby
1    Pusher.trigger('my-channel', 'generate', {
2      url: 'http://example.com/file.pdf'
3    })

W trzecim parametrze przekazujemy wszystko, co zostanie przekazane do front-endu. W takim razie Twój worker powinien wyglądać w następujący sposób:

Ruby
1    class GeneratorWorker
2      include Sidekiq::Worker
3
4      def perform(*args)
5        # imagine heavy sql queries, data processing
6        sleep(30)
7
8        Pusher.trigger('my-channel', 'generate', {
9          url: 'http://example.com/file.pdf'
10      })
11     end
12   end

Powinniśmy teraz wdrożyć naszą funkcjonalność po stronie klienta. Na początku zawrzyjmy bibliotekę Pushera w aplikacji poprzez dodanie go do pliku application.js. Następnie musimy stworzyć instancję klienta Pushera. Pamiętaj żeby dodać swój klucz aplikacji!

Super, teraz nasz klient Pushera jest gotowy. Kolejnym krokiem jest przypięcie się do naszego kanału. Jak to się robi? To naprawdę proste, musimy tylko subskrybować wybrany kanał w taki sposób:

Ruby
1    var channel = pusher.subscribe('my-channel');

Jeśli chcemy, by dane były pobierane z odpowiedniej akcji gdy jest ona włączona, to powinniśmy dodać:

Ruby
1    channel.bind('generate', function(data) {
2      $('#result').html(data.url);
3    });

W tym przypadku, gdy jakieś dane wysyłane są z serwera do klienta możemy zrobić z nimi co nam się podoba.

Ruby
1    //= require rails-ujs
2    //= require pusher
3    //= require jquery
4    //= require jquery_ujs
5    //= require_tree .
6
7    Pusher.logToConsole = true;
8
9    var pusher = new Pusher('your_key', {
10     cluster: 'eu',
11     encrypted: true
12   });
13
14   $(document).on('ready', function() {
15     $('#generate_data').click(function(event) {
16       var channel = pusher.subscribe('my-channel');
17       event.preventDefault();
18
19       $.ajax({
20         method: 'GET',
21         url: '/home/generate',
22         beforeSend: function() {
23           $('#result').html('Processing...');
24         },
25         success: function(data) {
26           channel.bind('generate', function(data) {
27             $('#result').html(data.url);
28           });
29         }
30       });
31     });
32    });

Dodatkowo dodałem możliwość wyświetlanie wszystkiego w konsoli przeglądarki, by móc upewnić się, że wszystko jest w porządku.

Ruby
1    Pusher.logToConsole = true;

Teraz powinniśmy zrestartować Foremana, ponieważ zmieniliśmy kod w workerze oraz dodaliśmy zmienne środowiskowe. Kiedy ponownie spróbujesz stworzyć joba w workerze poprzez naciśnięcie przycisku na stronie, zobaczysz następujący komunikat:

Tak, ponieważ użyliśmy Sidekiq’a i zadanie było wykonywane w tle nasze zapytanie zajęło tylko 4 milisekundy! Nieźle, co? Nasze zapytania do aplikacji nie są już dłużej wstrzymywane i blokowane!

Jak widzisz przeprowadzenie całego zadania zajęło właśnie 30 sekund — ale już w tle.

Możesz sprawdzić wszystko w konsoli przeglądarki — sprawdzić co się dzieje i zdebugować swój kanał. Jest to naprawdę użyteczne.

Podsumowanie

Mam nadzieję, że powyższy tutorial będzie użyteczny, a jego przeczytanie pomoże Ci w poprawieniu funkcjonalności Twojej aplikacji i sprawi, że będzie ona działała szybciej i bardziej niezawodnie. W przypadku jakichkolwiek pytań zostaw poniżej komentarz. Dziękuję za uwagę i poświęcony czas!


Artykuł został przetłumaczony z bloga nopio.com.

Podobne artykuły

[wpdevart_facebook_comment curent_url="https://geek.justjoin.it/sidekiq-pusher-dlaczego-warto-uzywac-razem/" order_type="social" width="100%" count_of_comments="8" ]