tim.jarratt

tim.jarratt

Hey all. I’ve been working on a system lately that needs to ingest 10s of thousands to millions of rows from a data lake in order to do some post-processing of that data. We’ve decided to try using the Stream api for handling the ingestion of data from the data lake.

The real code I’m working with is a bit complex, but I’ve reproduced a simplified version of what the code really looks like

defmodule IngestionWorker do

  @doc """
  1. Query raw data from an external system
  2. Save it to the database
  3. Persist the state of our work process (so it knows where to start again)
  """
  def perform_ingest(cursor) do
    {total, last_row} =
    query_new_data_to_ingest(cursor)
    |> Stream.map(&process_row/1)
    |> count_elements(returning_last: true)

    # persist the total number of rows and last row seen to implement a simple cursor
    persist_ingestion_state(total, last_row.id)
  end

  # pretend that this is actually ingesting data from an external system
  # here we ignore the cursor, but imagine this cursor is used in a WHERE clause
  # to only select rows that we haven't seen yet
  defp query_new_data_to_ingest(_cursor) do
    1..1_000_000
    |> Stream.map(fn id -> %{id: id, interesting_data_we_need: "row_#{id}"} end)
  end

  defp process_row(record) do
    # the line below causes a memory leak
    RawRecord.changeset(record) |> insert!()
  end

  defp insert!(changeset) do
    changeset

    # MyCoolApp.Repo.insert!(changeset)
  end

  defp count_elements(stream, opts) do
    result =
      stream
      |> Stream.transform(0, fn value, acc -> {[{acc + 1, value}], acc + 1} end)
      |> Stream.take(-1)
      |> Enum.to_list()

    {count, last} =
      case result do
        [] -> {0, nil}
        [{count, last}] -> {count, last}
      end

    Tracer.set_attribute(:stream_count, count)

    if Keyword.get(opts, :returning_last, false),
      do: {count, last},
      else: count
  end

  defp persist_ingestion_state(_total, _last_seen_id) do
    # implementing a cursor based system is left as an exercise for the reader
    nil
  end
end

defmodule RawRecord do
  @moduledoc """
  Persists the state of raw data received from external systems.

  This data may be incomplete, wrong, or be otherwise low in quality.

  We persist this data in its raw form to decouple ingestion from enrichment in clean domain tables

  Additionally this serves as an audit log of how data has changed over time (this is important)
  """

  use Ecto.Schema
  import Ecto.Changeset

  schema "raw_records" do
    field :interesting_data_we_need, :map

    timestamps()
  end

  def changeset(record \\ %__MODULE__{}, attrs) do
    record
    |> cast(Map.new(attrs))
    |> validate_required(~w[interesting_data_we_need]a)
  end
end

In our private function process_row/1 we want to persist each row using an Ecto schema. When we run this code, it quickly results in a growing heap. However, if we change the implementation as below, we don’t get a memory leak

  defp process_row(record) do
    RawRecord.changeset(record) |> insert!()

    # avoid a memory leak by discarding the result
    nil
  end

This works for when we don’t need to read the last value from the stream, but if we want to get the last value read from the stream, we would need to get that some other way (eg: issue another ecto query).

I have two questions.

  1. Why does consuming the stream result in increasing the heap size if we are only going to take the last value from the stream ? I’d love to understand the stdlib Stream implementation better :sweat_smile:

  2. Does anyone have any clever ideas for how we can consume the stream without causing a memory leak while still returning the last value ?

Showing Posts 1 to 1

dimitarvp

dimitarvp

To be fair, no, sorry.

Seeing as you are inserting stuff in DB then why can’t you just fetch the record that has the most recent value in inserted_at? :thinking: It would achieve the same without having to change a stream accumulator millions of times.

— All posts loaded —

Where Next? Top

Trending in Questions Top

Blokh
Hey guys, I’ve got a huge CSV ( around 10 GB ) that needs to be processed hourly Do you guys have any suggestions what is the best prac...
New
kszambelanczyk
Hello! Could someone please give me a help/sample code, how to delete a file from s3 using waffle/waffle_ecto from Phoenix app. I creat...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New
psy-q
I’m trying to set up Emacs with elixir-ls via lsp-mode and credo via Flycheck. This should mostly be preconfigured as Flycheck picks up c...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge & Solve. They are GUI (Emerge) and State management (S...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews