Skip to content

Instantly share code, notes, and snippets.

@joshnuss
Last active August 6, 2017 01:08
Show Gist options
  • Save joshnuss/1079985780a574cb2c97 to your computer and use it in GitHub Desktop.
Save joshnuss/1079985780a574cb2c97 to your computer and use it in GitHub Desktop.
High speed e-commerce checkout using Elixir & Task.async
# Parallel Checkout
# --------------------------------------------------------------
# Example of performance gained by using a parallel checkout in an e-commerce store,
#
# to run 500 checkouts in series: time elixir checkout.exs 500 serial
# to run 500 checkouts in parallel: time elixir checkout.exs 500 parallel
#
# Typical E-commerce checkout flow uses a bunch of network bound tasks, that are generally
# computed synchronously. This wastes time and requires larger server clusters to handle peak times
#
# Here is an example of tasks you would see in a typical store's checkout:
#
# Pre-payment tasks:
#
# - Address verification for billing address: 0.4s
# - Address verification for shipping address: 0.4s
# - Check inventory with SAP: 0.25s per item
# - Check shipping rate with FedEx: 0.5s
# - Check shipping rate with UPS: 0.5s
# - Check tax rate with Avalara: 0.5s
# ------------------------
# In parallel: 0.5s, in series (2 items): 2.45s
#
# Payment task:
# - Process credit card with Stripe: 0.5s
#
# Post-payment tasks:
# - Notify Shipwire fulfilment: 0.4s
# - Notify Analytics: 0.2s
# - Notify MailChimp: 0.2s
# ------------------------
# In parallel/no-wait: 0s, in series: 0.8s
#
# TOTAL
# In parallel: ~1s, in series: ~3.75s
#
# This approach also increases throughput,
# because work is network bound, so multiple checkouts can run
# at the same time (eg. a 32 core machine could handle more than 32 checkouts at once)
# define a bunch of dummy services that sleep to simulate work:
defmodule Stripe.Gateway do
def authorize(_credit_card) do
:timer.sleep(500)
{:authorized, "1234-12345"}
end
end
defmodule SAP.Inventory do
def check(_product) do
:timer.sleep(250)
:in_stock
end
end
defmodule Avalara.TaxRate do
def compute(_address, _amount) do
:timer.sleep(500)
1.07
end
end
defmodule ShippingRate do
defmodule FedEx do
def compute(_address, _items) do
:timer.sleep(500)
10.99
end
end
defmodule UPS do
def compute(_address, _items) do
:timer.sleep(500)
15.99
end
end
end
defmodule AddressVerification do
def verify(_address) do
:timer.sleep(400)
:ok
end
end
defmodule Shipwire.Fulfilment do
def notify(_order) do
:timer.sleep(300)
:ok
end
end
defmodule Analytics do
def notify(_order) do
:timer.sleep(200)
:ok
end
end
defmodule MailChimp do
def notify(_order) do
:timer.sleep(200)
:ok
end
end
# struct to hold cart data: addresses + totals + items
defmodule Cart do
defstruct number: "",
item_total: 0,
total: 0,
tax: 0,
shipping: 0,
billing_address: nil,
shipping_address: nil,
credit_card: nil,
items: []
end
defmodule Checkout do
# build a dummy cart with 2 items
def build_cart do
%Cart{number: "12345", items: [
%{sku: "COKE-CLSC", name: "COKE Classic", quantity: 1, price: 1.99},
%{sku: "DR-PEPPER", name: "Dr. Pepper", quantity: 2, price: 2.09}
]}
end
# run in series
def run_serial do
cart = build_cart
# run address verification
:ok = AddressVerification.verify(cart.billing_address)
:ok = AddressVerification.verify(cart.shipping_address)
# check stock for each item
[:in_stock, :in_stock] = Enum.map(cart.items, &SAP.Inventory.check/1)
# find minimum shipping rate
fedex_shipping_rate = ShippingRate.FedEx.compute(cart.shipping_address, cart.items)
ups_shipping_rate = ShippingRate.UPS.compute(cart.shipping_address, cart.items)
shipping_rate = min(fedex_shipping_rate, ups_shipping_rate)
# compute tax amount
tax_amount = Avalara.TaxRate.compute(cart.billing_address, cart.items)
# compute item total
item_total = Enum.reduce cart.items, 0, fn (item, acc) ->
acc + (item.quantity * item.price)
end
# update cart with totals
cart = %{cart | item_total: item_total, tax: tax_amount, shipping: shipping_rate, total: item_total + shipping_rate + tax_amount}
# process credit card
{:authorized, _authorization} = Stripe.Gateway.authorize(cart.credit_card)
# notifiy shipwire, mailchimp & analytics
Shipwire.Fulfilment.notify(cart)
MailChimp.notify(cart)
Analytics.notify(cart)
# return total
cart.total
end
# run in parallel
def run_parallel do
cart = build_cart
# create an array of tasks
# all will run in parallel (at the same time)
tasks = [
Task.async(AddressVerification, :verify, [cart.billing_address]),
Task.async(AddressVerification, :verify, [cart.shipping_address])
]
++ Enum.map(cart.items, &(Task.async(SAP.Inventory, :check, [&1])))
++ [
Task.async(ShippingRate.FedEx, :compute, [cart.shipping_address, cart.items]),
Task.async(ShippingRate.UPS, :compute, [cart.shipping_address, cart.items]),
Task.async(Avalara.TaxRate, :compute, [cart.billing_address, cart.items]),
]
# wait for all tasks to complete
results = Enum.map(tasks, &Task.await/1)
# pattern match to pull out useful data
[:ok, :ok, :in_stock, :in_stock, fedex_shipping_rate, ups_shipping_rate, tax_amount] = results
# compute totals
shipping_rate = min(fedex_shipping_rate, ups_shipping_rate)
item_total = Enum.reduce cart.items, 0, fn (item, acc) ->
acc + (item.quantity * item.price)
end
# update cart variable with totals
cart = %{cart | item_total: item_total, tax: tax_amount, shipping: shipping_rate, total: item_total + shipping_rate + tax_amount}
# process card
{:authorized, _authorization} = Stripe.Gateway.authorize(cart.credit_card)
# create a list of services to notify
notification_list = [Shipwire.Fulfilment, MailChimp, Analytics]
# create a list of notification tasks, and dont bother waiting for completion
Enum.each(notification_list, &(Task.async(&1, :notify, [cart])))
# return total
cart.total
end
end
# check argv
times = System.argv |> List.first |> String.to_integer
result = case Enum.at(System.argv, 1) do
# Run in series (slow), time to run is the sum of each operation (increases with the more items you have in cart)
"serial" ->
Enum.map 1..times, fn _ ->
Checkout.run_serial
end
# Run in parallel (super fast), can use all cores. on a 32 core server, completion is tied to the single longest operation
"parallel" ->
tasks = Enum.map 1..times, fn _ ->
Task.async(Checkout, :run_parallel, [])
end
Enum.map(tasks, &Task.await/1)
end
IO.inspect(result)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment