Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Scaling Concurrency Without Getting Burned

Scaling Concurrency Without Getting Burned

We all know Elixir is fast and scalable, but knowing how to leverage its strengths can be the difference between a fast program and one unrivaled by other platforms. Whether you're reaching for Tasks and GenServers, or wrangling data with Ecto, understanding subtle nuances of each tool is the key to avoiding bottlenecks and writing efficient applications.

Starting from real-world scenarios, we'll explore various OTP tricks and concurrency patterns to drive maximum performance. Along the way, we'll see how features built-in to Elixir and OTP can apply to applications from small to large. We'll also navigate the unexpected pitfalls that come with cheap concurrency and how to safeguard from each trap.

Whether you're an intermediate or seasoned Elixir developer, you'll leave with some new performance tricks and a mental model for writing highly concurrent applications.

Alex Garibay

August 31, 2018
Tweet

More Decks by Alex Garibay

Other Decks in Programming

Transcript

  1. 1

  2. 2

  3. 3

  4. 4

  5. 5

  6. 6

  7. 7

  8. 8

  9. 9

  10. 10

  11. 11

  12. 12

  13. def handle_call({:fetch_block, block_number}, state) do # Fetch block data from

    third-party API block_data = fetch_block(block_number) {:reply, block_data, state} end 13
  14. 14

  15. 15

  16. 19

  17. 20

  18. 22

  19. 23

  20. 24

  21. 25

  22. 26

  23. 27

  24. 28

  25. 29

  26. 30

  27. 31

  28. 32

  29. 33

  30. 34

  31. 35

  32. def init(_) do state = build_state() send(self(), :start_task) {:ok, state}

    end def handle_info(:start_task, state) %Task{ref: ref} = task = Task.async(fn -> do_expensive_work(state) end) Task.run(task) {:noreply, Map.put(state, :task_ref, ref)} end 36
  33. 37

  34. def handle_info({ref, expensive_work_result}, %{task_ref: ref} = state) # Flush messages

    related to the monitor Process.demonitor(ref, [:flush]) # Do something with the result process_result(expensive_work_result) send(self(), :start_task) {:noreply, %{state | task_ref: nil}} end 38
  35. 39

  36. 40

  37. 41

  38. def handle_info(:start_task, state) do %Task{ref: ref} = Task.Supervisor.async_nolink( BlockScout.TaskSupervisor, fn

    -> do_expensive_work(state) end ) {:noreply, Map.put(state, :task_ref, ref)} end 43
  39. 44

  40. def handle_info({ref, expensive_work_result}, %{task_ref: ref} = state) # Flush messages

    related to the monitor Process.demonitor(ref, [:flush]) # Do something with the result process_result(expensive_work_result) send(self(), :start_task) {:noreply, %{state | task_ref: nil}} end 45
  41. 46

  42. def handle_info({:DOWN, ref, :process, pid, reason}, %{task_ref: ref} = state)

    do # Decide how to respond to a failure handle_process_failure() {:noreply, %{state | task_ref: nil} end 47
  43. 48

  44. 49

  45. 50

  46. 54

  47. 56

  48. 57

  49. query = unfetched_address_balances_query() reducer = fn (item, list) -> [item

    | list] end initial = [] Repo.transaction( fn -> query |> Repo.stream() |> Enum.reduce([], reducer) end ) 59
  50. query = unfetched_address_balances_query() reducer = fn (item, list) -> [item

    | list] end initial = [] Repo.transaction( fn -> query |> Repo.stream(timeout: :infinity) |> Enum.reduce([], reducer) end, timeout: :infinity ) 60
  51. 61

  52. defmodule BlockScout.MarketRates.Cache do use GenServer # ... def handle_info(:refresh_rates, _state)

    do rates = fetch_rates() {:noreply, rates} end def handle_call({:fetch_ticker, ticker}, state) do {:reply, Map.get(state, :ticker), state} end end 62
  53. 63

  54. 64

  55. def init(_) do :ets.new( :market_rates_cache, [:set, :named_table, :public, read_concurrency: true,

    write_concurrency: true] ) send(self(), :refresh_rates) {:ok, %{}} end 65
  56. def handle_info(:refresh_rates, state) do rates = fetch_rates() insert_into_ets(data) {:noreply, state}

    end def all_tickers do :ets.tab2list(:my_app_cache) end def fetch_ticker(ricker) do case :ets.lookup(:market_rates_cache, ticker) do [item] -> {:ok, item} _ -> {:error, :not_found} end end 66
  57. 67

  58. 68

  59. 73

  60. 74