Kalo te dokumentacioni
Ruby

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.

config/initializers/openemail.rb
api_key = Rails.application.credentials.dig(:openemail, :api_key) OpenEmail.init(api_key:, timeout: 15) if api_key

Ruajeni ç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ë.

app/jobs/send_invoice_job.rb
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])  endend

retry_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.

app/sidekiq/welcome_email_job.rb
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}"    )  endend

Nga 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.

app/services/invoice_email.rb
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"    )  endend

InvoiceEmail.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.

app/controllers/open_email_webhooks_controller.rb
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  endend
config/routes.rb
Rails.application.routes.draw do  post "/webhooks/openemail", to: "open_email_webhooks#create"end

Një 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.

app/controllers/open_email_webhooks_controller.rb
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  endend

Kalojani 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.

app/webhooks/open_email_webhook.rb
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"]]  endend

Parametrat 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.

app/controllers/uploads_controller.rb
class UploadsController < ApplicationController  def create    file = OpenEmail.files.upload(params.require(:file))     render json: {id: file[:id]}  endend

Fijet 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.

test/jobs/send_invoice_job_test.rb
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.