[HN Gopher] Thinking in Actors - Challenging your software model...
___________________________________________________________________
Thinking in Actors - Challenging your software modelling to be
simpler
Author : jeremycarter
Score : 112 points
Date : 2024-12-01 22:16 UTC (1 days ago)
(HTM) web link (jeremycarterau.substack.com)
(TXT) w3m dump (jeremycarterau.substack.com)
| yodon wrote:
| Huge fan of building systems on top of Orleans Actors (one of the
| tools discussed in the article)
| dirtbag__dad wrote:
| Does anyone care to share an example of the OrderService that is
| not anemic?
| jeremycarter wrote:
| It's tricky to provide a really good example right now. I
| notice in the .NET world there are like these DDD "starter
| packs", and my god, they're just layers of maintainability
| hell. If you look at older OOP/DDD books you'll notice that the
| domain object has real world methods on it, just like as if it
| were from a UML diagram.
|
| What you should end up with are plain OOPy objects that mirror
| the real world. They're not skewed or constrained by their
| database model. They shouldn't have any dependencies on your
| infrastructure layer. The object should encapsulate state,
| behaviour, validity and consistency.
|
| An example (which is probably overkill) would be
| https://learn.microsoft.com/en-us/dotnet/architecture/micros...
|
| The next post is about modelling an actor and it might provide
| more insight for you.
| ibgeek wrote:
| It's not clear from the article whether actors offer
| significant benefits (or disadvantages) for data modeling
| versus the traditional OO paradigm. The article reads more
| like an introduction that describes the problem and teases a
| solution rather a complete article that offers a solution and
| evaluation of it.
| jeremycarter wrote:
| That's fair feedback. I wanted to post it in 3 parts, but I
| see now I probably should have just made one large post.
| macintux wrote:
| You could always post the 3 parts simultaneously, so
| anyone who wants to dig deeper can continue into the
| series.
| zamalek wrote:
| Some actor frameworks (Erlang, Orleans) would interpret each
| order being an actor instance, and each order line being an
| actor instance. To make that clearer: each order row would
| _logically_ have an actor running somewhere in your cluster.
| This would mean that an order actor would, by nature, have a
| "add order line" receiver (i.e. method). Each is in charge of
| how its data is stored. In reality, you'd _probably_ only do
| this stuff on the command end: query would hit
| tables+joins+whatever directly.
|
| If you work this back to DDD, then the Order entity would have
| an AddOrderLine method. The central service would be
| responsible for handing out Order entities and saving them once
| modified.
|
| Up until a few years ago (prior to my previous job) I was a
| hypothetical believer in DDD and actors as two equally viable
| alternatives. I am now _strongly_ against DDD, but still agree
| with the article on a hypothetical basis about actors.
| svieira wrote:
| What did you learn about (by using?) DDD that made you decide
| strongly against it?
| zamalek wrote:
| DDD advocates for creating a shared lingo across everyone
| involved, including customers if they are highly embedded
| in the design process. This I agree with.
|
| Ultimately DDD attempts to create real-world analogies in
| code; you know, dog inherits from pet inherits from mammal
| etc. In my opinion, this approach to OOP easily ends up
| creating code that is difficult to reason about. Probably
| because real-world things often have many responsibilities.
| Code becomes especially confusing when you have dozens of
| methods on domain objects that interact across several
| domains: the system-wide control flow becomes extremely
| complex. Now add outlier code/hacks, likely written to meet
| an unrealistic deadlines, and things rapidly become
| completely incomprehensible.
|
| And there's more that's hard to put to words. I code for
| the love of it, and I truly hated every moment working in
| DDD code. That was I completely novel experience for me:
| I'm fine with boring work (it has to happen), but DDD just
| hit very differently.
| valiant55 wrote:
| This sounds like DDD done wrong. Just because two
| concepts have the same name doesn't mean that they are
| the same thing. Drawing the boundaries of the bounded
| contexts is hard though, which is why shops often
| struggle with DDD.
|
| For example if I'm building a pharmacy system a
| prescription means something to a patient, but also means
| something different (but similar) to a fulfillment team
| member. The prescription might have a prescriber, and its
| important for the patient to know the name, address and
| contact information of the prescriber. But for
| fullfulment purposes I don't care about the address or
| phone number, just the NPI, full name and title for
| labeling purposes. This doesn't just extend to data, but
| to actions, a patient can't "ship" a prescription and
| fullfulment can't "renew" a prescription. In a DDD model
| these should be two separate objects.
| jeremycarter wrote:
| I agree. I'm not advocating doing a purist DDD solution but
| rather using similar modelling techniques and just making
| naive actors.
| ibgeek wrote:
| The article seems to be smashing together two (seemingly)
| unrelated topics and doesn't offer much in the way of a solution.
| What alternative design does the author propose? Is it possible
| to solve the problem with traditional object-oriented design
| techniques? It's not clear that the issues presented require or
| substantially benefit from the actor model without seeing a best
| in-class OO example.
| jeremycarter wrote:
| As I mentioned there's nothing novel in this post, especially
| for a senior. This is more about getting some context out of
| the way so that I can show some techniques in a future post.
| karmakaze wrote:
| I suspect that a lot of what can be done with actors can be done
| in Go using the "Don't communicate by sharing memory, share
| memory by communicating". Basically a consumer of a channel can
| act like an actor. What we don't get for free is the activation
| and resiliency. A closer one would be Erlang/Elixir on the BEAM.
| On the more advanced end would be using F# or Pony.
|
| Curious to see what the follow-up parts will post. I am
| interested in actor model programming and want to see different
| ways of understanding and using them.
| suraci wrote:
| > a lot of what can be done with actors can be done in Go using
| the "Don't communicate by sharing memory, share memory by
| communicating"
|
| Yes, it is logically correct since CSP and Actors are solutions
| for the same problem(concurrency)
|
| And I do think CSP is easier to use compare with Actors as a
| programming model, but when it comes to distributed and
| business applications, the comparative advantage is reversed
| asplake wrote:
| Why F# there?
| karmakaze wrote:
| I was thinking of Mailbox Processor and available Actor
| libraries. Here's a pretty good rundown I found for some[0]:
|
| > Actor Model in .NET
|
| > In the .NET world, there are at least a few battle-tested
| frameworks that allow you to build stateful systems using the
| actor model. The famous ones are Akka.NET[1], Orleans[2],
| Proto.Actor[3], and Dapr[4]. All of these systems support
| distributed mode, as well as a bunch of other features like
| actor supervision, virtual actor, parent-child hierarchies,
| reentrancy, distributed transactions, dead letter queue,
| routers, streams, etc. In addition to these frameworks, .NET
| has several lightweight libs that do not have many of the
| listed features but still allow the use of the actor model.
| For example, F# has built-in support via Mailbox Processor[5]
| and also there the toolkits: TPL Dataflow[6] and Channel[7].
| TPL Dataflow and Channel are not actor model implementations
| but rather foundations for writing concurrent programs with
| the ability to use the actor model design pattern.
|
| [0] https://medium.com/draftkings-engineering/entering-actor-
| mod...
|
| [1] https://getakka.net/
|
| [2] https://dotnet.github.io/orleans/index.html
|
| [3] https://proto.actor/
|
| [4] https://docs.microsoft.com/en-
| us/dotnet/architecture/dapr-fo...
|
| [5] https://www.codemag.com/Article/1707051/Writing-
| Concurrent-P...
|
| [6] https://docs.microsoft.com/en-
| us/dotnet/standard/parallel-pr...
|
| [7] https://devblogs.microsoft.com/dotnet/an-introduction-to-
| sys...
| CharlieDigital wrote:
| Clarification: Dapr's actor system is not limited to
| .NET[0]
|
| It currently supports .NET, Java, and Python
|
| [0] https://docs.dapr.io/developing-applications/building-
| blocks...
| debugnik wrote:
| These days, for F# I'd use a task builder loop with
| System.Threading.Channels and TaskCompletionSource, rather
| than the MailboxProcessor.
|
| Same thing, really; you could recreate the MailboxProcessor
| API with those in just a few lines.
| jerf wrote:
| I view actors like I view "object orientation" in general. In
| fact they're not even all that dissimilar, being respectively
| "store and manipulate state attached to a single runtime
| context" (be it "thread" or "green thread" or async context or
| whatever) and "store and manipulate state attached to a single
| highly-structured value". (They go together pretty well,
| actually.) The primary value comes from the idea, and it can
| and should be managed and manipulated for the local context.
|
| Many people will come along and insist that you need every last
| thing according to some particular definition (e.g., in OO,
| "you need multiple inheritance and all of public, private, and
| protected, and you need virtual methods and you need and you
| need and you need OR YOU DON'T HAVE TRUE OBJECT ORIENTATION AND
| EVERYTHING WILL FALL APART"), and will insist that everything
| at every layer of your program will need to be structured in
| this paradigm OR YOU DON'T HAVE TRUE WHATEVER AND EVERYTHING
| WILL FALL APART, but both claims are observably false.
|
| I use actors in nearly every program I write, in almost any
| language I write in anymore if there's any concurrency at all.
| But they are a _wonderful_ tool to drop in somewhere, and
| encapsulate some particularly tricky bit of concurrent logic,
| like an in-memory concurrent cache, without having to
| restructure everything to be All Actors All The Time. I spent a
| lot of years in Erlang and still sort of consider this a
| mistake, up there with Java trying to "force" everything into
| OO by forcing everything to be in a class, even if it has no
| business being there. You don't need to go down someone's check
| list and dot every i and cross every t "or you don't have
| _true_ actors ". It's very much an 90/10 situation where the
| vast bulk of the benefit is obtained as soon as you have
| anything even actor-ish, and the additional marginal benefit of
| going all the way down to some very precise definition can be
| marginal and may even be negative if it requires you to contort
| your code in some unnatural way just to fit into some framework
| that isn't benefiting you on this particular task (e.g., Erlang
| has a lot of nice tools but if you're building a purely-local
| command-line app with it for whatever reason you probably don't
| have a great need for clustering support or OTP in general).
| jeremycarter wrote:
| My sentiments exactly - well said. The big mistake is forcing
| OOP onto everything. It's why I really have an affinity with
| the virtual actor, it has just enough OOP for me to have
| classes, methods and internal state - I don't need
| inheritance, polymorphism, etc - just naive little
| classifications of things we call objects.
|
| I also don't have to think of my system in a hierarchical
| supervised manner like earlier actor models. I just need
| cloud native distributed little objects.
|
| You can see some modern products coming out that are just
| that: 1. Cloudflare Durable Objects -
| https://developers.cloudflare.com/durable-objects/ 2. Restate
| Virtual Objects - https://restate.dev/
| LaserToy wrote:
| I love actors as a concept and I heard some large companies
| (Expedia) implemented large parts using them.
|
| But I also saw how hard it is to understand a large system that
| built using actors. It is just hard to comprehend all the
| communication pathways and what happens in the system.
| jspdown wrote:
| When the design closely aligns with the real world problem it
| solves, communication pathways are natural and you don't really
| have to care much about them. What matters is the Actor's role
| and making sure it represent a strong domain concept. The rest
| follows naturally.
|
| But to be fair, it's never that simple and you always end up
| with some part of a system that's less "well-designed". In that
| case,figuring out who talks to who can quickly become a
| nightmare.
|
| Actors are great on the paper, but to benefit from them, you
| need great understanding of your domain. I tend to use it later
| in the development process, on specific part where the domain
| is rich and understood.
| jghn wrote:
| > It is just hard to comprehend all the communication pathways
| and what happens in the system.
|
| Having worked on large scale actor-based systems before, I'll
| attest this is quite true. However, what often gets lost in
| these conversations is that this is also true of large scale
| OOP based systems as well.
|
| If one takes a few steps back and squints, there's really not
| much difference between Objects and Actors: in both cases you
| have a namespaced entity (object, actor) that receives signals
| via some defined mechanism (methods, messages) which lead it to
| perform some action.
| rdtsc wrote:
| > But I also saw how hard it is to understand a large system
| that built using actors.
|
| Indeed, it can be just as much of a spaghetti mess as any other
| code, but it becomes easier if actors are the preferred
| abstraction for a platform already, for instance as it is for
| Erlang/Elixir on the BEAM VM.
|
| The platform comes with a few benefits such as:
| 1) Immutable data: inside each actor the state is explicitly
| evolved from one message to the next. It's passed as an
| explicit argument to functions. Erlang is even better as the
| variable binding itself is immutable. 2) Isolated
| heaps: actors all have isolated heaps. You can have millions of
| them per OS process and they can't reach in and modify each
| other's memory. They have to send and receive a message.
| 3) Supervision trees: actors that work together can be grouped
| into a tree hierarchy so that if one starts, it start the
| others and they have "links" between them. If some crash,
| others crash with them. After the crash they can be restarted
| safely. It can be done safely because they have isolated heaps.
| Restarting a bunch of OS threads in a regular C/Java/etc
| program cannot be done safely, usually. These supervision
| hierarchies is how the system can be organized. A top level
| actor might serve as the API endpoint for its children so
| message go through it. 4) Tracing/live debugging:
| every message that is sent or function call can be traced
| dynamically by connecting to a live system. That can be helpful
| of making sense of the mess when debugging.
|
| There are many "actor" systems out there. It's not a big deal
| to write a function to send a message to a lockless "mailbox"
| to be received by a thread in pretty much any modern
| language/platform. Doing that seems like it gets you 90% there
| to "actors", but without those 4 points above it only gets
| there 10% of the way. You can build a quick demo, but it would
| become a nightmare in a production system.
| dboreham wrote:
| Some background:
| https://en.m.wikipedia.org/wiki/Communicating_sequential_pro...
| gpderetta wrote:
| Better link: https://en.wikipedia.org/wiki/Actor_model
|
| CSP is a bit different from the actor model (see the comparison
| section in the page you linked).
|
| The biggest difference is that CSP communication is
| synchronous.
| kiitos wrote:
| Isolation: Since actors process messages they receive
| sequentially, there are no concurrency issues within an actor.
| This simplifies reasoning about state mutation and transitions.
| ... Fault Tolerance: State can be persisted (e.g., to
| storage, database or event log) between messages. If an actor
| crashes, it can recover its state on another node and resume
| processing.
|
| The system model in which each actor instance is single-threaded,
| processes received requests individually and sequentially, and
| can "crash" in a way that affects only the in-flight request, is
| a total anachronism, irrelevant since more than a decade, at any
| meaningful scale.
| t-writescode wrote:
| It sounds like you may know a lot about this; but each actor
| operating in a "single-threaded", "do one thing at a time on
| your own" paradigm is what I'm familiar with, especially with
| Akka.NET, where my experience lies.
|
| Please tell me more about its irrelevance, etc, I'd love to
| learn!
| packetlost wrote:
| I don't see how this isn't still both possible and entirely
| relevant to modern systems. An actor is not necessarily a
| system process/thread. It has more to do with scope/context
| than any particular execution model. Think more like a request
| handling context in an async language.
| kiitos wrote:
| It's true that an actor is not necessarily a system process.
| It can be a process, or a thread, or a coroutine, or etc. But
| "crashing" isn't anywhere near so ambiguous. Crashing doesn't
| mean the request fails, or the thread gets killed -- crashing
| means the underlying process terminates.
|
| That may not be how _some folks_ understand the concept of
| crashing, but it 's definitely how _most folks_ understand
| it, at least insofar as they write software.
|
| 1 service instance needs to be able to handle O(1k+)
| concurrent requests at scale, and a failure in any given
| request can't impact other in-flight requests. Those failures
| aren't crashes, and that software isn't crash-only -- using
| those terms just obfuscates things, and makes everything
| harder for everyone.
| jerf wrote:
| Any time you are communicating between multiple programming
| language communities, it is important to understand that
| they _will_ have differing definitions for things, to
| extend grace to people trying to communicate across those
| barriers, and to not apply dogmatic definitions of terms
| that apply to the contexts you happen to be familiar with
| but are used differently elsewhere.
|
| "Crash" is not a universally defined term and you will find
| there are plenty of communities that do not agree that
| "crash == OS process terminating".
| kiitos wrote:
| For sure, yes. But terms of art, like "crash", generally
| have commonly-understood definitions. And the commonly-
| understood definition of the term of art "crash" is that
| it means OS process terminating. Not always! Not all the
| time. But in general, yes, that's what it means, to most
| people, most of the time.
| jatins wrote:
| is this AI generated? I flagged because seemed very low on
| substance and just generic bullet points
| jeremycarter wrote:
| It's just part 1. I have just published part 2 which goes into
| more detail.
| phtrivier wrote:
| > State can be persisted (e.g., to storage, database or event
| log) between messages. If an actor crashes, it can recover its
| state on another node and resume processing.
|
| And that's the part I never managed to solve, personally. The
| state of actors tend to look "a lot like" your domain objects,
| but you don't want to store it until the very end.
|
| Do you have a data-model for each actor to store their own
| snapshots ? When do you snapshot ? When do you sync with the
| "ground truth" ? Do you have different tech for each kind of
| state (eg "term storage" for actors, then relational db for
| ground truth ?)
| dkersten wrote:
| This is what I've struggled with when trying to learn
| erlang/elixir/gleam:
|
| If I go all in, then modelling as actors makes sense. The
| domain model is modelled as in-memory actors, using erlang
| features for persistence and what not.
|
| But most of the time this isn't what we want in modern
| software: instead our state is in a database and we are
| executing in a request-response setup. This seems to be
| mismatched with actors, as, it seems to me, at least, you're
| mapping the database states into your actors on each request
| and then back on response, at which point, what do actors
| actually buy you?
|
| The same mismatch exists with OO too, of course, but with
| actors there are a bunch of benefits that you get by going all
| in, which it seems to me are lost if you're simply building a
| database backed request-response system.
|
| I mean, many of the OP's pros of actors rely on actors being
| long lived things, rather than short lived within a request.
|
| Maybe I'm just missing something. But maybe it's also just not
| suited to typical web api backend?
| pjm331 wrote:
| a few years back i had one side project that i did in
| elixir/phoenix felt like a good fit specifically because it
| had a lot of state that needed to live outside of the
| request/resp cycle but also didn't live in a db
|
| Specifically the project was an app that ran terraform
| commands inside an actor and streamed the logs to the browser
|
| each actor had a terraform config and each message was a
| command to run on that config
|
| so in that case the actor model felt like it aligned really
| well with how the program needed to work
| withinboredom wrote:
| > When do you snapshot ?
|
| In Durable PHP (an actor system for PHP), it tries to achieve
| at-most-once processing guarantee (though more often than not,
| it is exactly-once).
|
| 1. commit the current state
|
| 2. send outgoing messages
|
| 3. ack the original event
|
| If we fail before 1, we simply retry
|
| If we fail between 1-3, we simply rewind and retry
| phtrivier wrote:
| Then, it assumes that your actors don't have any side
| effects. (Otherwise, "retrying" is not an option.)
|
| I found that surprisingly hard in most applications - and I
| think that's is a limitation of the actor model _in
| practice_, that is often overlook because it's only an
| implementation detail that does not really exists _in
| theory_.
| withinboredom wrote:
| Durable PHP has a way to guarantee exactly-once side
| effects through "activities" (I assume you are talking
| about the outside world side-effects, not internally).
| Trying to do the same activity twice will simply result in
| merging with the previous execution. This isn't integrated
| with actors -- mostly because I hadn't thought about it
| before. It wouldn't be hard to do though.
|
| However, in most async systems, you can only guarantee at-
| most-once or at-least-once; exactly-once is quite hard (and
| why there is a dedicated way to do it in this framework).
|
| Internally, messaging is set up to guarantee at-least-once
| and duplicate messages are ignored (as well as dedicated
| ways to ensure deterministic execution).
| jonathanpglick wrote:
| This is where erlang/elixir really shines! Using actor processes
| (aka genservers) to model business workflows and logic helps to
| align the programmer's and stakeholder's shared understanding and
| language of a feature, which leads to the implementation and
| expectations about it to be much more "correct".
|
| Too many of us jump straight to modeling the domain objects as
| database tables without formalizing the the data model of the
| actual business need. Explicit state changes and considering
| "time" are way more important than database layout at that point.
|
| And please either use operation explicit types and/or finite
| state machines when modeling the main domain objects.
|
| My last three jobs started with untangling a bunch of "status"
| fields into explicit and hierarchical structures so that the
| whole company could agree and define what each of them actually
| meant. Common language, yo! It's the secret sauce!
___________________________________________________________________
(page generated 2024-12-02 23:01 UTC)