30

2 years after the first event - The Saga Pattern

Embed Size (px)

Citation preview

Page 1: 2 years after the first event - The Saga Pattern
Page 2: 2 years after the first event - The Saga Pattern

2 years after the first event - The Saga Pattern

Page 3: 2 years after the first event - The Saga Pattern

Message Driven Architecture● Commands

● Events

Page 4: 2 years after the first event - The Saga Pattern

Commandsparams,

form objects,

real command objects

module Seating

class BookEntranceCommand

include Command

attribute :booking_id, String

attribute :event_id, Integer

attribute :seats, Array[Seat]

attribute :places, Array[GeneralAdmission]

validates_presence_of :event_id, :booking_id

validate do

unless (seats.present? || places.present?)

errors.add(:base, "Missing seats or places")

end

end

end

end

Page 5: 2 years after the first event - The Saga Pattern

Eventsmodule Seating

module Events

SeatingSetUp = Class.new(EventStore::Event)

SeatingDisabled = Class.new(EventStore::Event)

EntranceBooked = Class.new(EventStore::Event)

EntranceUnBooked = Class.new(EventStore::Event)

end

end

Page 6: 2 years after the first event - The Saga Pattern

Send PDF via Postal

1. PostalAddedToOrder2. PostalAddressFilledOut 3. PdfGenerated4. PaymentPaid

5. => SendPdfViaPostal

Page 7: 2 years after the first event - The Saga Pattern

How did we get there?

Page 8: 2 years after the first event - The Saga Pattern

Publish events

class RegisterUserService def call(email, password) user = User.register(email, password) event_store.publish(Users::UserRegisteredByEmail.new( id: user.id, email: user.email, )) endend

Page 9: 2 years after the first event - The Saga Pattern

Publish events

event_store.publish(SeasonPassConnectedToAConference.new({ pass: { internal_id: pass.id, external_id: pass.external_id, }, season_id: season.id, booking_id: booking_id(event, pass),

id: ticket.id, event_id: event.id, ticket_id: ticket.id, ticket_type_id: ticket.ticket_type_id, barcode: ticket.barcode,

seat: { uuid: seat.uuid, label: seat.label, category: seat.category, },

holder: { user_id: holder.user_id, email: holder.email, name: holder.name, },}))

Page 10: 2 years after the first event - The Saga Pattern

Introducehandlers

module Search class UserRegisteredHandler singleton_class.prepend(YamlDeserializeFact)

def self.perform(event) new.call(event) rescue => e Honeybadger.notify(e) end

def call(event) Elasticsearch::Model.client.index( index: 'users', type: 'admin_search_user', id: event.data.fetch(:id), body: { email: event.data.fetch(:email) }) end endend

Page 11: 2 years after the first event - The Saga Pattern

Introducehandlers

class WelcomeEmailForImportedUsers singleton_class.prepend(YamlDeserializeFact) @queue = :other

def call(fact) data = fact.data organizer = User.find(data.fetch(:organizer_id)) organization = Organization.find(data.fetch(:organization_id)) season = Season.find(data.fetch(:season_id))

config = Seasons::Config.find_by_organizer_id(organizer.id) || Seasons::Config.new( class_name: "Seasons::Mailer", method_name: "welcome_imported_season_pass_holder", )

I18n.with_locale(organization.default_locale) do config.class_name.constantize.send(config.method_name, organization: organization, email: data.fetch(:email), reset_password_token: data.fetch(:password_token), season_name: season.name, organizer_name: organizer.name, ).deliver end endend

Page 12: 2 years after the first event - The Saga Pattern

One class handles multipleevents

TicketRefunded: stream: "Order$%{order_id}" handlers: - Scanner::TicketRevokedHandler - Salesforce::EventDataChangedHandler - Reporting::Financials::TicketRefundedHandler - Seating::ReleasePlaces

TicketCancelledByAdmin: stream: "Order$%{order_id}" handlers: - Scanner::TicketRevokedHandler

module Scanner class TicketRevokedHandler singleton_class.prepend(YamlDeserializeFact)

def call(event) data = event.data revoke = Scanner::Revoke.new revoke.call( barcode: data.fetch(:barcode), event_id: data.fetch(:event_id), ) end endend

Page 13: 2 years after the first event - The Saga Pattern

Handler+State=Saga

class CheckBankSettingsSaga singleton_class.prepend(::YamlDeserializeFact)

class State < ActiveRecord::Base self.table_name = 'check_bank_settings_saga' end

def call(event) data = event.data case event when ConferenceCreated, ConferenceCloned store_event(data) when TicketKindCreated check_bank_settings(data) end end

Page 14: 2 years after the first event - The Saga Pattern

Handler+State=Saga

def store_event(data) State.create!( event_id: data[:event_id], organization_id: data[:organization_id], organizer_id: data[:user_id], checked: false ) end

def check_bank_settings(data) State.transaction do record = State.lock.find_by_event_id(data[:event_id]) return if record.checked? record.update_attribute(:checked, true)

Organizer::CheckBankSettings.new.call( record.organizer_id, record.organization_id, record.event_id ) end endend

Page 15: 2 years after the first event - The Saga Pattern

Paymentpaid too late

1. OrderPlaced2. OrderExpired3. PaymentPaid

4. => ReleasePayment

Page 16: 2 years after the first event - The Saga Pattern

How to implement a Saga?Technical details

Page 17: 2 years after the first event - The Saga Pattern

Sync handlersfrom EventStore

module Search class UserRegisteredHandler

def self.perform(event) new.call(event) rescue => e Honeybadger.notify(e) end

def call(event) Elasticsearch::Model.client.index( index: 'users', type: 'admin_search_user', id: event.data.fetch(:id), body: { email: event.data.fetch(:email) }) end endend

Page 18: 2 years after the first event - The Saga Pattern

Contain exceptionsin sync handlers

module Search class UserRegisteredHandler

def self.perform(event) new.call(event) rescue => e Honeybadger.notify(e) end

endend

class RegisterUserService def call(email, password) ActiveRecord::Base.transaction do user = User.register(email, password) event_store.publish(UserRegisteredByEmail.new( id: user.id, email: user.email, )) end endend

Page 19: 2 years after the first event - The Saga Pattern

Async handlers

def call(handler_class, event)

if handler_class.instance_variable_defined?(:@queue)

Resque.enqueue(handler_class, YAML.dump(event))

else

handler_class.perform(YAML.dump(event))

end

end

Page 20: 2 years after the first event - The Saga Pattern

Async handlers(after commit)

def call(handler_class, event) if handler_class.instance_variable_defined?(:@queue) if ActiveRecord::Base.connection.transaction_open? ActiveRecord::Base. connection. current_transaction. add_record( FakeActiveRecord.new( handler, YAML.dump(event)) ) else Resque.enqueue(handler_class, YAML.dump(event)) end else handler_class.perform(YAML.dump(event)) end end

https://blog.arkency.com/2015/10/run-it-in-background-job-after-commit/

Page 21: 2 years after the first event - The Saga Pattern

YAML

module Search class UserRegisteredHandler singleton_class.prepend(::YamlDeserializeEvent)

def self.perform(event) new.call(event) rescue => e Honeybadger.notify(e) end endend

module YamlDeserializeFact def perform(arg) case arg when String super(YAML.load(arg)) else super end endend

Page 22: 2 years after the first event - The Saga Pattern

Re-raise exceptionsin async handlers

module Search

class UserRegisteredHandler

@queue = :low_priority

def self.perform(event)

new.call(event)

rescue => e

Honeybadger.notify(e)

raise

end

end

end

Page 23: 2 years after the first event - The Saga Pattern

Initializing the state of a saga

class PostalSaga singleton_class.prepend(YamlDeserializeFact) @queue = :low_priority

# add_index "sagas", ["order_id"], unique: true class State < ActiveRecord::Base self.table_name = 'sagas'

def self.get_by_order_id(order_id) do transaction do yield lock.find_or_create_by(order_id: order_id) end rescue ActiveRecord::RecordNotUnique retry end end

def call(fact) data = fact.data State.get_by_order_id(data.fetch(:order_id)) do |state| state.do_something state.save! end endend

Page 24: 2 years after the first event - The Saga Pattern

Processing an event by a saga

class Postal::FilledOut singleton_class.prepend(YamlDeserializeFact) @queue = :low_priority

def self.perform(event) new().call(event) rescue => e Honeybadger.notify(e, { context: { event: event } } ) raise end

def call(event) data = event.data order_id = data.fetch(:order_id) State.get_by_order_id(order_id) do |state| state.filled_out( filled_out_at: Time.zone.now, adapter: Rails.configuration.insurance_adapter, ) end endend

Page 25: 2 years after the first event - The Saga Pattern

Triggering a command

class Postal::State < ActiveRecord::Base def added_to_basket(added_to_basket_at:, uploader:) self.added_to_basket_at ||= added_to_basket_at save! maybe_send_postal_via_api(uploader: uploader) end

def filled_out(filled_out_at:, uploader:) self.filled_out_at ||= filled_out_at save! maybe_send_postal_via_api(uploader: uploader) end

def paid(paid_at:, uploader:) self.paid_at ||= paid_at save! maybe_send_postal_via_api(uploader: uploader) end

def tickets_pdf_generated(generated_at:, pdf_id:, uploader:) return if self.tickets_generated_at self.tickets_generated_at ||= generated_at self.pdf_id ||= pdf_id save! maybe_send_postal_via_api(uploader: uploader) end

Page 26: 2 years after the first event - The Saga Pattern

Triggering a command

class Postal::State < ActiveRecord::Base private

def maybe_send_postal_via_api(uploader:) return unless added_to_basket_at && paid_at && filled_out_at && tickets_generated_at return if uploaded_at

uploader.transmit(Pdf.find(pdf_id))

self.uploaded_at = Time.now save! rescue # error handling... endend

Page 27: 2 years after the first event - The Saga Pattern

Triggering a command (better way)

class Postal::State < ActiveRecord::Base

private

def maybe_send_postal_via_api return unless added_to_basket_at && paid_at && filled_out_at && tickets_generated_at return if uploaded_at

self.uploaded_at = Time.now save!

command_bus.send(DeliverPostalPdf.new({ order_id: order_id, pdf_id: pdf_id })) endend

https://github.com/pawelpacana/command_bus

Page 28: 2 years after the first event - The Saga Pattern

Thank you!Make events, not CRUD

Page 29: 2 years after the first event - The Saga Pattern

35% DISCOUNTwith code

WROCLOVE2016

https://blog.arkency.com/products/

Page 30: 2 years after the first event - The Saga Pattern

Resources

● original saga patterns○ http://vasters.

com/clemensv/2012/09/01/Sagas.aspx○ http://kellabyte.com/2012/05/30/clarifying-the-

saga-pattern/ ● saga - process manager

○ https://msdn.microsoft.com/en-us/library/jj591569.aspx

○ http://udidahan.com/2009/04/20/saga-persistence-and-event-driven-architectures/

● other○ http://verraes.net/2014/05/functional-

foundation-for-cqrs-event-sourcing/○ http://stackoverflow.

com/questions/15528015/what-is-the-difference-between-a-saga-a-process-manager-and-a-document-based-ap

○ http://theawkwardyeti.com/comic/moment/