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

fklement
This is a thread to gather some information about the efforts of using elixir in combination with cars or vehicular systems in general. ...
New
adamu
When starting a new project, do you have any go-to libraries you pull in out of habit/preference? For example, Ash, Credo, Mox, ExMachin...
New
fireproofsocks
I’m not a Lambda fan-boy because I think it’s overprescribed as a cure-all for every possible problem when in reality, Lambdas are best s...
New
rump13
Hi everyone, I’ve been following Elixir since around 2016 and tinkering with it on and off, but I haven’t had the opportunity to use it ...
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
billylanchantin
If you’re not familiar with this fun bit of math lore, Paul Erdős, the famously eccentric, peripatetic and prolific 20th-century mathem...
New
markmark206
Every time I build a web app, I worry about a bunch of basic things (persisting data, knowing when to compute which things, keeping thing...
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
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
dogweather
I’ve been brainstorming about ways to solve the N-dimensional code organization problem, and am thinking about developing a Smalltalk-lik...
New

Other popular topics Top

Qqwy
Update: How to use the Blogs &amp; Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3268 119930 1237
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
josevalim
Hi everyone, One of the features added to Elixir early on to help integration with Erlang code was the idea of overridable function defi...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1140 51847 244
New
William
I would like to know that is there any online source for learning Phoenix Framework for building E-Commerce Store? Any advantage on build...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
Nvim
Elixir appears to be a superior language to Python. I don’t see any advantage of Python over Elixir. Are there any?
New

We're in Beta

About us Mission Statement