jdumont

jdumont

Opinion on file & memory based event sourcing system

I could write forever about this, but I’ll do my best to keep it succinct.

For anyone familiar with event sourcing, what is your opinion of an ES system that logs all it’s events to a file, and keeps all of it’s projections in memory. It would be a cross between Joe’s love of term_to_binary and Martin Fowler’s memory image.

Whilst it seems like an artificial constraint — no external DB dependence — I think it could be an interesting system and provide some advantages, mainly around speed courtesy of the memory image and easy transport/storage of data (S3 or perhaps even Git for some ES inception!)

I know (and use) projects like Commanded, so understand that ES is much more involved in reality than just reducing over a list of events — the mechanics of how Commanded works are not to be underestimated — but I think that a simpler implementation of ES might be warranted in some cases.

This idea has been bouncing around my head for a few weeks, and I’ve done a few experiments — mainly using Erlang’s disk_log, but I think DETS and mnesia are worth exploring — but thought I’d get some other opinions on the concept before venturing further. Worth putting time into, or should I just continue using Commanded? :rofl:

Most Liked

benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

We’ve built GitHub - CargoSense/fable for that kind of thing at Cargoesnse, although sadly there’s basically 0 documentation right now.

The core idea is that in your application you often have “root” database tables and sort of auxilliary tables. As a simple example, we have a trips table, and then there are things like alarms, trip grants, trip statistics, and so forth. We wanted to emit events on a given trip, and ensure that all updates to the trips table itself and the associated alarms, grant, and statistics all came from those events.

Here is a basic example out of our actual code:

The basic concept is you have a simple mapping of event struct names to handler functions:

defmodule Maven.Events do
  use Fable.Events,
    repo: Maven.Repo

  alias Maven.{Accounts, Travel}

  def handlers() do
    %{
      Travel.TripStarted => &Travel.trip_started/2,
      Travel.TripEnded => &Travel.trip_ended/2,
      Travel.TripGrantIssued => &Travel.trip_grant_issued/2,
      Travel.TripGrantRevoked => &Travel.trip_grant_revoked/2,
       ...
    }
  end
end

And then your context functions emit an event:

def start_trip(current_user, animal, tracker, %{id: id} = attrs) do
    Repo.serial(%Trip{id: id}, fn trip ->
      with :ok <- current_user |> can(:start_trip, %{animal: animal}),
           :ok <- animal_available(animal, current_user),
           :ok <- tracker_registered(tracker),
           :ok <- tracker_available(tracker) do
        event = %TripStarted{
          trip_id: id,
          animal_id: animal.id,
          started_by_id: current_user.id,
          tracker_id: tracker.id,
          started_at: Map.get(attrs, :started_at, DateTime.utc_now())
        }

        Events.emit(trip, event)
      else
        error -> error
      end
    end)
  end

The %Trip{} struct is a 100% ordinary Ecto schema, it just has the addition of a field(:last_event_id, :integer, read_after_writes: true). This is used by Fable to guarantee that events are processed serially, and no events are skipped. The Trip{} row acts as basically the aggregate state in ES terms.

The %TripStarted{} event is just an ecto embedded schema, which gets written to the database when we call emit. Then Fable runs the specified handler functions, passing each the trip aggregate and the emitted event, and it’s this handler function’s job to go update the trip database row and any other associated rows that should react to this event:

def trip_started(trip, event) do
    attrs =
      event
      |> Map.from_struct()
      |> Map.put(:id, trip.id)

    trip_changes = Trip.changeset(trip, attrs)

    with {:ok, trip} <- Repo.insert(trip_changes) do
      create_initial_grants(trip, event.started_by_id)
      maybe_create_test_data(trip)
    end
  end

All of the above is done in a single database transaction.

Importantly, Fable does NOT require CQRS. You’re totally allowed to just query the trips table for read state too. It is absolutely event sourcing though, since all writes to the trips table and its associated tables start with an event and it guarantees that all events are processed in order for a single aggregate. And since it’s all just Ecto and postgres, so Ecto async tests work out of the box.

If you wanted to do it CQRS style though you totally could by making your event handler functions only update the aggregate row state, and then driving all your read model changes off of a process manager. Each Fable process manager is a GenServer backed by a database row that tracks progress through the event log, and thus you could use that to manage a separate set of read tables.

If there’s sufficient interest I’ll try to get some docs up.

15
Post #4
dokie

dokie

Our team at Inflowmatix built our ES/CQRS into our platform using a combination of meta-programming, protocols and functional Elixir. We originally ran our platforms domain store (behind a protocol to allow switching) on Mnesia for around 18 months in Production. We used term_to_binary along with compression and made heavy use of snapshots of our Aggregate state to reduce Aggregate startup costs/times. Eventually though, as our most active Aggregates started to have a long history and frequent snapshots, the table fragmentation in Mnesia was getting heavy. Additionally the locking model at a table level for certain transactions caused frequent rollbacks and retries with lots of conflict resolutions, which eventually converge, but with heavy load can cause upstream timeouts on GenServers especially where a call was needed to assure the transaction completed.

Eventually after many small repairs and refactors we switched our store to Postgresql and we were able to use our protocol to write a splitter module that wrote to both stores for a while (and read from Postgresql) until we migrated the historic data in the background to Postgresql then switched the splitters behaviour to use only Postgresql.

Just some notes really to perhaps bear in mind.

jdumont

jdumont

I’m still chipping away at this whenever I get a moment, and thought that I’d provide a quick update on where I’m at.

I tried using many different options for storing events, all using a shared interface so that I could quickly switch between them. I’ve found some wonderful little features of both Elixir and Erlang and some cool libraries to help me along the way — it really has been a brilliant learning experience!

So far I’ve tried:

  • Simple term_to_binary and File.open(x [:binary, :append])
  • DETS
  • Erlang’s disk_log
  • Erlang’s file:consult for reading events back and a custom io_lib:format function to write them
  • CubDB (using min_key and max_key for event number ranges was brilliant)

All had positives and negatives with disk_log probably coming out on top. It has a load of features that you really need for working with files already baked in and thoroughly tested. The deal breaker though was that it’s really hard to get events back out by their event number, as disk_log really doesn’t work with any keys, it literally just appends a given term to the end of the log.

Getting the events out required bringing all events into memory (ignoring disk_logs wonderful chunking feature) and filtering out events older than those we were interested in. This isn’t the worst thing in the world as I was planning on keeping logs partitioned by aggregate, meaning that they are unlikely to ever become so long that parsing the whole log becomes an issue as it’s incredibly fast - loading 100_000 average-sized events in approximately 75ms.

My concern was that I could end up with a compromised method for reading and writing the literal heart of this system.

The next phase in this experiment was yet more learning… Martin Kleppmann’s “Designing Data Intensive Applications”. I can honestly say that I learnt more reading this in a week than I did over the previous 9 months piecing together articles and documentation online. His closing conclusion of “turning the database inside out” is very in line with what I’m trying to accomplish here, although I definitely think he was talking at a much larger scale than I’m working at! :joy:

Importantly, the book highlighted many issues that I was unwittingly making with other parts of my system. I was neglecting a total order by writing all of my events in partitions based on aggregates. It wasn’t a deal-breaker though as I could accept partial order as a trade-off in this instance as causality isn’t an issue in my domain. However, these partitions did make my projections quite complex due to needing to track their event offsets (using consumer offsets over ack) for many, many, many streams. Not an issue but one that I decided that for now at least to avoid by opting for a single unified log with total order.

I’m using a very simple writer and index pattern from the book and at the moment it’s working very well. Sending a command to an aggregate, having it validated, events created, persisted and applied to the aggregate is taking on average around 150 micro-seconds. I still have to handle log rotation and snapshots, but these should be quite simple as I’m modelling my store as a series of GenStage (yet another thing I’ve learnt since starting this).

I’ve decided in many cases to opt for purposely naive approaches - whereas before they were just naive :wink: - in order to keep my system simple. My tests have shown that I’ve got a good amount of performance margin with which to make compromises such as the single log which is a potential bottleneck, but hugely simplifies the entire system. Equally, a single event stream, local-only Registry makes it a lot easier to work with and understand. I figure that it’s easier to make a system that already works well faster, than it is fix a fast system thats yet to work.

I have had crisis’ of confidence along the way — “Why aren’t you just using Commanded and PostgreSQL like a sensible person?” - “You’re totally out of your depth here!” - “Why even bother with event sourcing, CRUD could work here after all” — but overall I’m very happy with the progress I’ve made and the things I’ve learnt. I think I’m probably a little way off being able to build anything complex with it yet, but I’m enjoying the process.

jdumont

jdumont

Hey, I’m glad that the thread has been useful. Event sourcing is a bit of a bear to get your head around initially, and for a “simple” pattern, there’s a lot of complexity hidden in implementing it well. I’ll confess that I’m still not there myself and will keep using Commanded whilst slowly tapping away at this idea.

You’re very right that storing events leaves you bound to your past mistakes or oversights. That’s pretty much the biggest cost of the pattern, if you screw up you either have to live with it or spend a lot of time managing previous versions of your events, or migrating them to the new structure. There’s some good links floating around the forum (just search “eventsourcing”) that dive into this issue further.

For this reason, it’s often recommended to use ES in a well understood domain with a lot of time spent up front working out the data your events need. It’s not a tool to pull off the shelf when you’re still finding your way through the problem.

For what it’s worth I handle the event versioning issue by being quite overzealous in the data that each event captures. You can be quite objective with event data as it’s “something that happened” so there’s only so many ways that you can interpret it. Make sure you store everything that may be relevant to this action and you should be OK. I’ve only had one instance in the last year where I needed a new version of an event, and it was because the original was missing a data point I didn’t think was required at the time. Fortunately it was easy to add in and provide my handlers a fallback value where the older values were missing it.

One thing that often gets overlooked and which tripped me up was the boundaries of my aggregates. Perhaps because I don’t have a DDD background, but where my aggregates sat in relation to my projectors caused me some grief early on.

As for storing the function within the event: personally I’m not sure it would work. It’s fine for mapping the behaviour of that event within the aggregate, but doesn’t allow for the possibly dozens of ways this event will be used by a countless number of handlers. Not to mention you lose what is wonderful about ES: data separated from logic and therefore the ability to rewrite the handlers and operate on old data as your requirements change. It’s a nice idea and I do think that “logic as data” has it’s place, I just wouldn’t use it here.

dimitarvp

dimitarvp

Exactly because I don’t want external DBs I am working on bringing in sqlite3 to Ecto 3. Technically it is an external DB but it all works in-process so no external server to manage. I’d go for one on-disk sqlite3 DB and another one that is in memory.

However, the rest is up to you so I don’t think it’s what you are looking for. Just giving you an idea for a storage backend.

Where Next?

Popular in Discussions Top

heathen
Quite interesting article Google brought me. Didn’t find any mentions about it here. What do you think in general? Would you use togethe...
New
hauleth
Maybe some of you know that there was (is?) something like BERT-RPC which in short is simplified version of External Term Format (also kn...
New
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
AstonJ
@Garrison’s comment in another thread reminded me of this post by Joe: With the big five exerting more control than ever, new (AI) play...
New
derpycoder
So, anyone got a chance to look at this?!? I’m kind of glad this came along. We can just throw this into our Auth pages and won’t have t...
New
bartblast
This thread is dedicated to announcing updates to the Hologram documentation, as well as discussing any ideas for improvements and sugges...
New
AstonJ
A remark @Garrison made… ..has already inspired a thread for the technical side of reclaiming our internet, but there is of course anot...
New
adam
I wonder if anyone in the community has started to use Github’s Codespaces for their team’s development? If so, what is there to learn? ...
New
chrisliaw
Hi, I’m wondering is it my thinking process or this is the norm among the Elixir developer for the use of Struct and accessor functions ...
New
AstonJ
Inspired by Andrew’s post in another thread about types: If the main benefit of static typing is to catch errors, and most people think...
New

Other popular topics Top

yurko
Here are few pieces of (common) Linux knowledge that we use for reasonably small one server apps. We use Ubuntu but this should work for ...
New
dotdotdotPaul
Okay, I'm having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I'm sure I'...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 record...
New
ycv005
I have followed this StackOverflow post to install the specific version of Erlang. And When I am running mix ecto.setup then getting fol...
New
malloryerik
Hi, this is for people who, like me, have had some friction using .html.heex templates in VSCode. The solution seems to be, in a hyphena...
New
lk-geimfari
What is most correct way to open, read and parse JSON file with poison? For example if we have example.json file in root of some projec...
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
msaraiva
Surface is an experimental library built on top of Phoenix LiveView and its new LiveComponent API that aims to provide a more declarative...
564 42633 214
New

We're in Beta

About us Mission Statement