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!
Spis treści
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.