Last active
August 6, 2017 01:08
-
-
Save joshnuss/1079985780a574cb2c97 to your computer and use it in GitHub Desktop.
High speed e-commerce checkout using Elixir & Task.async
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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