Rails dhe Rack
Një inicializues, një punë në sfond që nuk mund të dërgojë dy herë, një kontrollues webhook-u dhe teste që nuk e prekin kurrë rrjetin.
Konfigurimi
Gem-i nuk ka integrim të vetin me Rails: as Railtie, as metodë dërgimi për ActionMailer. API-në e thërrisni nga një punë në sfond ose nga një objekt shërbimi, përmes klientit të përbashkët ose një klienti që e ndërtoni vetë, njësoj si nga çdo program Ruby.
api_key = Rails.application.credentials.dig(:openemail, :api_key) OpenEmail.init(api_key:, timeout: 15) if api_keyRuajeni çelësin me bin/rails credentials:edit, nën openemail dhe api_key. Pas OpenEmail.init, OpenEmail.emails, OpenEmail.webhooks dhe çdo hapësirë tjetër emrash e përdorin atë klient në çdo kërkesë, punë në sfond dhe sesion konsole të procesit.
Pa kredencialin, klienti i përbashkët ndërtohet vetë në thirrjen e tij të parë nga OPENEMAIL_API_KEY, ndaj një vendosje (deploy) që e mban çelësin në mjedis nuk ka nevojë fare për inicializues. OpenEmail.init ngre ArgumentError kur nuk gjen kredencial askund, dhe një inicializues ekzekutohet edhe për komanda si assets:precompile, ku çelësi mund të mos jetë vendosur; prandaj thirrja më lart është e mbrojtur me kusht.
Dërgimi nga një punë në sfond
Dërgoni nga një punë në sfond dhe jo nga kërkesa, që një dërgim i ngadaltë ose që dështon të mos e mbajë kurrë peng një faqe. Nxirreni idempotency_key: nga regjistri të cilit i përket dërgimi. Një punë që ekzekutohet sërish, sepse radha e riprovoi ose një worker ra pasi API-ja u përgjigj, dërgon atëherë me të njëjtin çelës, dhe API-ja e riluan mesazhin që e ka dërguar tashmë në vend që të dërgojë një të dytë.
class SendInvoiceJob < ApplicationJob queue_as :default retry_on OpenEmail::NetworkError, wait: 30.seconds, attempts: 5 discard_on OpenEmail::ValidationError def perform(invoice) sent = OpenEmail.emails.send( from: "Acme Billing <[email protected]>", to: invoice.customer_email, subject: "Invoice #{invoice.number}", html: "<p>Your invoice #{invoice.number} is attached.</p>", attachments: [{filename: "#{invoice.number}.pdf", content: invoice.pdf.download, contentType: "application/pdf"}], idempotency_key: "invoice:#{invoice.id}:email" ) invoice.update!(email_id: sent[:id], email_status: sent[:status]) endendretry_on OpenEmail::NetworkError ia kthen radhës një dërgim që nuk mori përgjigje, dhe i njëjti çelës e bën ekzekutimin e radhës një riluajtje nëse i pari arriti vërtet te API-ja. discard_on OpenEmail::ValidationError heq një mesazh që API-ja e refuzoi ashtu siç ishte shkruar, sepse dërgimi i tij sërish nuk mund të ketë sukses. Kur një NetworkError arrin te puna, gem-i e ka riprovuar tashmë vetë dërgimin, dy herë si parazgjedhje, me të njëjtin çelës.
Mbajeni çelësin të qëndrueshëm për regjistrin dhe qëllimin, ashtu si invoice:42:email. Një çelës i ndërtuar nga një vulë kohore ose nga SecureRandom është i ri në çdo ekzekutim, dhe atëherë një punë e riprovuar do të dërgonte dy herë. Ripërdorimi i një çelësi me një trup tjetër, si një faturë që ndryshoi midis dy ekzekutimeve, refuzohet me një 422 idempotency_key_reuse në vend që të dërgohet.
invoice.pdf.download kthen bajtet nga Active Storage si String binar, të cilin content i një bashkëngjitjeje e merr ashtu siç është. Një kohë nga Active Support, si scheduledAt: 1.day.from_now, dërgohet si çast UTC si çdo Time, ndërsa Date.tomorrow është një Date e Ruby-t, që dërgohet si datë e zhveshur që API-ja e lexon si mesnatë UTC, ndaj jepni një kohë kur ora ka rëndësi.
Një punë e thjeshtë Sidekiq funksionon njësoj. Nxirreni çelësin nga argumentet e punës dhe lërini riprovat e vetë Sidekiq-ut ta riluajnë dërgimin.
class WelcomeEmailJob include Sidekiq::Job def perform(user_id, email) OpenEmail.emails.send( from: "Acme <[email protected]>", to: email, subject: "Welcome to Acme", text: "Glad you are here.", idempotency_key: "welcome:#{user_id}" ) endendNga një objekt shërbimi
Një objekt shërbimi që e merr klientin si argument, me atë të përbashkët si parazgjedhje, e mban dërgimin në një vend të vetëm dhe i lejon një testi t’i japë një klient të vetin.
class InvoiceEmail def initialize(client: OpenEmail.client) @client = client end def deliver(number:, to:) @client.emails.send( from: "Acme Billing <[email protected]>", to:, subject: "Invoice #{number}", text: "Your invoice #{number} is attached.", idempotency_key: "invoice:#{number}:email" ) endendInvoiceEmail.new.deliver(number: "INV-2026-0042", to: "[email protected]") dërgon përmes klientit të përbashkët, dhe InvoiceEmail.new(client: test_client) përmes çdo klienti tjetër.
Marrja e webhook-eve
Verifikoni çdo dërgesë para se të veproni sipas saj. Rails e analizon një trup JSON te params, por nënshkrimi mbulon bajtet e papërpunuara, ndaj jepini verifikuesit request.raw_post bashkë me request.headers, nga ku ai lexon header-in e nënshkrimit.
class OpenEmailWebhooksController < ActionController::API def create event = OpenEmail.verify_webhook_signature( payload: request.raw_post, headers: request.headers, secret: Rails.application.credentials.dig(:openemail, :webhook_secret) ) HandleOpenEmailEventJob.perform_later(event) head :no_content rescue OpenEmail::WebhookSignatureError head :bad_request endendRails.application.routes.draw do post "/webhooks/openemail", to: "open_email_webhooks#create"endNjë kontrollues ActionController::API nuk ka mbrojtje kundër falsifikimit që duhet anashkaluar. Nën ActionController::Base, ose një ApplicationController të ndërtuar mbi të, shtoni skip_forgery_protection, përndryshe Rails e refuzon POST-in para se të ekzekutohet veprimi juaj, sepse një dërgesë nuk mbart token autenticiteti.
class OpenEmailWebhooksController < ApplicationController skip_forgery_protection def create event = OpenEmail.verify_webhook_signature( payload: request.raw_post, headers: request.headers, secret: Rails.application.credentials.dig(:openemail, :webhook_secret) ) HandleOpenEmailEventJob.perform_later(event) head :no_content rescue OpenEmail::WebhookSignatureError head :bad_request endendKalojani ngjarjen një pune në sfond dhe përgjigjuni menjëherë: një dërgesë që nuk merr përgjigje brenda 5 sekondave llogaritet e dështuar dhe dërgohet sërish më vonë. id e ngjarjes është e njëjtë në çdo riprovim dhe riluajtje të saj, ndaj ruani id-të që keni trajtuar dhe kapërceni ato që i keni parë.
head :bad_request i përgjigjet një dërgese të falsifikuar ose të vjetruar. Një sekret që mungon është një dështim tjetër: verify_webhook_signature ngre për të ArgumentError, të cilin kontrolluesi nuk e kap, ndaj një aplikacion i konfiguruar keq përgjigjet me 500 dhe dërgesa provohet sërish sapo ta rregulloni, në vend që çdo ngjarje të refuzohet si e falsifikuar.
I njëjti kontroll funksionon në çdo aplikacion Rack, me vetë env-in e Rack-ut si headers:, ku header-i i nënshkrimit vjen si HTTP_X_OPENEMAIL_SIGNATURE. Një endpoint Rack si ky funksionon më vete me run OpenEmailWebhook.new te config.ru, ose brenda Rails me mount OpenEmailWebhook.new => "/webhooks/openemail" te rrugëzimet.
class OpenEmailWebhook def call(env) request = Rack::Request.new(env) event = OpenEmail.verify_webhook_signature( payload: request.body.read, headers: env, secret: ENV.fetch("OPENEMAIL_WEBHOOK_SECRET") ) HandleOpenEmailEventJob.perform_later(event) [204, {}, []] rescue OpenEmail::WebhookSignatureError [400, {"content-type" => "text/plain"}, ["bad signature"]] endendParametrat dhe ngarkimet
Trupi i një kërkese mund të jetë çdo objekt që i përgjigjet to_hash, ndaj ActionController::Parameters të lejuara kalojnë drejtpërdrejt. Një skedar nga një formular, një ActionDispatch::Http::UploadedFile, shkon drejt e te files.upload, që merr prej tij emrin dhe tipin e skedarit.
class UploadsController < ApplicationController def create file = OpenEmail.files.upload(params.require(:file)) render json: {id: file[:id]} endendFijet e ekzekutimit dhe fork-et
Një klient i vetëm mund të ndahet pa rrezik mes fijeve të ekzekutimit, ndaj fijet e Puma-s dhe worker-at e Sidekiq-ut mund të dërgojnë të gjithë njëkohësisht përmes OpenEmail.emails. Klienti është i ngrirë sapo ndërtohet, grupi i lidhjeve pas tij merr një kyç, dhe po ashtu edhe OpenEmail.init dhe OpenEmail.client.
Pas një fork-u, si në modalitetin cluster të Puma-s me preload_app!, në Unicorn ose Resque, procesi bir i harron lidhjet që trashëgoi dhe hap të vetat, ndaj një klient i ndërtuar në një inicializues para fork-ut është i sigurt në çdo worker. Nuk ka asgjë për t’u rilidhur te on_worker_boot.
Çdo proces mban deri në 8 lidhje boshe për host, për 2 sekonda secila. Për ta ndryshuar këtë, jepni OpenEmail::NetHttpAdapter.new(max_idle:, keep_alive_timeout:) tuajin si adapter:.
Testimi
Jepini klientit një adapter në testet tuaja dhe asgjë nuk largohet nga makina. OpenEmail.init te setup e zëvendëson klientin e përbashkët për gjithë procesin, dhe OpenEmail.reset_client te teardown e heq atë, ndaj thirrja e radhës ndërtohet sërish nga mjedisi.
require "test_helper" class SendInvoiceJobTest < ActiveJob::TestCase FakeOpenEmail = Struct.new(:requests) do def call(request) requests << request OpenEmail::HttpResponse.new( status: 200, headers: {"content-type" => "application/json"}, body: JSON.generate({id: "msg_test", status: "sent"}) ) end end setup do @api = FakeOpenEmail.new([]) OpenEmail.init(api_key: "oe_test_fake", adapter: @api, max_retries: 0, disable_update_notice: true) end teardown do OpenEmail.reset_client end test "a job that runs twice sends under one key" do invoice = invoices(:september) 2.times { SendInvoiceJob.perform_now(invoice) } keys = @api.requests.map { |request| request.headers["Idempotency-Key"] } assert_equal ["invoice:#{invoice.id}:email"] * 2, keys assert_equal "/emails", URI(@api.requests.first.url).path endendÇdo kërkesë e regjistruar është një OpenEmail::HttpRequest me method, url, headers, body dhe timeout, dhe body i saj është JSON-i që do të ishte dërguar, ndaj JSON.parse(request.body) tregon vetë mesazhin. Ktheni një status gabimi me zarfin e gabimit të API-së për të testuar si e trajton kodi juaj një refuzim.
Klienti i përbashkët i përket gjithë procesit, ndaj testet që ekzekutohen njëkohësisht në fije ekzekutimi duhet t’i japin kodit që testohet një klient të vetin, siç e pranon objekti i shërbimit më lart.