[HN Gopher] Ergo: Erlang-inspired event driven actor framework i...
       ___________________________________________________________________
        
       Ergo: Erlang-inspired event driven actor framework in Go
        
       Author : nateb2022
       Score  : 136 points
       Date   : 2024-09-12 11:06 UTC (11 hours ago)
        
 (HTM) web link (github.com)
 (TXT) w3m dump (github.com)
        
       | Xeoncross wrote:
       | > Zero dependencies
       | 
       | This is something seldom attempted, but I congratulate you. Go is
       | one of a few languages that really is batteries-included. Just
       | about anything you could need is included in the stdlib.
        
         | seanw444 wrote:
         | One of the reasons I prefer it over something like Rust for
         | most projects. I don't have to waste time figuring out what
         | third-party library is the defacto standard that I should be
         | using.
        
           | iknowstuff wrote:
           | It's a tradeoff. Do you prefer to be stuck with bad built-in
           | file and time APIs, or a robust ecosystem of external crates?
           | 
           | https://fasterthanli.me/articles/i-want-off-mr-golangs-
           | wild-...
        
             | hu3 wrote:
             | So your argument is that an ecosystem of external crates
             | which are created and maintained mostly by individual
             | contributors is on average better than the standard library
             | backed and dogfooded by trillion dollar company and used by
             | other giants of the industry? Can't say I agree.
             | 
             | Not to mention nothing prevents anyone from using or
             | writing their own library only for the parts that need
             | specialization. You're free to do that and many have.
             | 
             | And standard libraries can be versioned and deprecated too.
        
               | everforward wrote:
               | I actually wouldn't be surprised, if only because the
               | standard library is so much harder to make backwards-
               | incompatible changes to. I would generally expect that
               | the average quality of the third party libs is lower, but
               | the top 1% is probably better than stdlib.
               | 
               | Eg I don't find the stdlib logging library particularly
               | great; not bad, but not impressive. Ditto for the stdlib
               | errors package before they added error wrapping
        
             | mijamo wrote:
             | "robust ecosystem" is a rather optimistic view of the rust
             | situation... I would have said "a bunch of 0.x libraries
             | that do 80% of the work and let you figure you the hard 20%
             | while being slightly incompatible with each other, and that
             | will be painful to glue together because of the rules for
             | traits"
        
             | pjmlp wrote:
             | I rather have something that works everywhere there is a
             | full implementation of the platform, instead of whatever
             | third parties decided to support.
        
             | kbolino wrote:
             | The file API is mostly fine. Opening with that is the wrong
             | approach to me; the comparison with Rust doesn't really
             | illustrate any serious issues. All of the complaints could
             | be addressed with better documentation.
             | 
             | The time API, on the other hand, is that bad, and worse.
             | Besides the bizarre monotonic time implementation, there's
             | also the bloated size of the time.Time struct, and the lack
             | of actual "dates" (YYYY-MM-DD) and "times" (HH:MM:SS) that
             | you can do arithmetic on. You can't really roll your own
             | robustly either because time.Time isn't a great foundation
             | to build on and //go:linkname is getting locked down.
             | 
             | Thankfully, build constraints are much better now, since
             | they just use regular boolean operators, e.g.:
             | //go:build windows && (i386 || amd64)
             | 
             | I agree that there shouldn't be any _buildtag.go magic, and
             | I think they ought to remove that "feature" entirely in a
             | future version of Go (which can be done so that it doesn't
             | affect older or unversioned code). It seems they added a
             | "unix" build tag too.
             | 
             | Also, one of my personal gripes is that the standard
             | library exposes no guaranteed way to access the system
             | CSPRNG. Any code can replace (crypto/rand).Reader with
             | another implementation and compromise cryptographic
             | operations. It's a trivial supply-chain attack vector, and
             | even in non-malicious scenarios, it can be done mistakenly
             | and break things all over the place in a subtle but
             | dangerous way. The language developers have so far refused
             | to fix it too.
             | 
             | Then there's log/slog with no TRACE or FATAL levels built-
             | in; yeah, you _can_ roll your own levels, but why should
             | you have to?
        
               | pjmlp wrote:
               | Anyone with enough experience in C derived languages
               | large scale preprocessor spaghetti, welcomes
               | _buildtag.extension alternative.
               | 
               | This is actually one of the few things I fully agree with
               | Go designers.
        
               | kbolino wrote:
               | Is the file foo_bar.go (no build tag line) compiled or
               | not?
               | 
               | If your Go version doesn't know of such a build tag as
               | "bar", then foo_bar.go is unconditionally compiled. If Go
               | in a later version adds "bar" as a known build tag, then
               | foo_bar.go becomes conditionally compiled. Better hope
               | you know this is how things work (reality: lots of Go
               | devs don't).
               | 
               | Build tag lines don't have this problem. They always
               | specify compilation conditions, even if the build tag is
               | not already known to the compiler. They also apply to the
               | whole file, the same as _tag in the name; there's no
               | preprocessor spaghetti possible.
        
             | Thaxll wrote:
             | Rust has multiple http server and async framework which
             | creates a lot of confusion.
        
             | metadat wrote:
             | Great example, and more of a horror show than I
             | anticipated.
             | 
             | It would have been interesting if the author had a
             | suggestion on what the Golang team should've / could've
             | done instead, beyond correctly communicating the nature of
             | the breaking change in the Go 1.9 release notes.
        
         | dlachausse wrote:
         | Interestingly, Erlang is very batteries included as well.
         | Probably even more so than Go in most cases.
        
           | worldsayshi wrote:
           | Does Elixir inherit those batteries or is the ecosystem
           | partially disconnected from Erlang?
        
             | brightball wrote:
             | Elixir inherits it. You can call anything in Erlang
             | directly.
        
         | pjmlp wrote:
         | Still doesn't have half of batteries that Python, Java and .NET
         | include.
        
           | philosopher1234 wrote:
           | No doubt that is true, but are there particular batteries
           | you're thinking of?
        
             | pjmlp wrote:
             | GUI, dependency injection, huge collection libraries,
             | dynamic runtime Interoperability, distributed objects,
             | compiler plugins, custom runtime schedulers,... for
             | example.
             | 
             | Some of the above will never be in Go due to how the
             | community and language designers are philosophically
             | against them.
        
               | limit499karma wrote:
               | > dependency injection
               | 
               | It's been a while since I played with the furry thing but
               | is that even possible in Golang?
        
               | pjmlp wrote:
               | I was speaking about Python, Java and .NET standard
               | libraries.
               | 
               | In Go you can naturally do it, by using the manual
               | constructor approach, however there is no magic auto
               | wiring like you can do with attributes and compiler
               | plugins, plus standard libraries infrastructure for
               | locating services, from those three ecosystems above.
        
               | limit499karma wrote:
               | Well, in my head DI at this point requires the magic
               | bits. Otherwise it is (as you say) just constructor args.
        
           | jddj wrote:
           | .net will even include every version of that battery since
           | the battery factory first prototyped it in 2002
        
           | nerdponx wrote:
           | Don't forget Gauche: https://practical-
           | scheme.net/gauche/man/gauche-refe/Finding-...
        
       | Xeoncross wrote:
       | Never having worked with Erlang, my mind is telling me this might
       | be an alternative for https://github.com/temporalio (database-
       | backed functions) or AWS Step functions.
        
         | seneca wrote:
         | They're not the same, exactly, but there are similarities. The
         | actor framework isn't inherently durable workflows, for
         | example. It does similarly distribute work though.
        
           | brightball wrote:
           | Supervisors give you the durability though.
        
             | jerf wrote:
             | A different kind of durability than what people mean by
             | "durable workflows", though. Durable workflows require
             | durable storage of their current state; supervisors give
             | you durable computation services. Supervisors don't even
             | guarantee "durable computation"; if a process crashes
             | halfway through processing a request, it's not like the
             | request will be automatically retried or something. (That
             | isn't even a good idea, there's a very good chance the
             | reason why that request crashed one process will crash
             | others.)
        
               | brightball wrote:
               | You can actually set it up that way though. You set a
               | process to keep the state and a process to do the work.
               | If the process doing the work crashes, the state is
               | preserved.
               | 
               | It's a deliberate decision but very easy with the BEAM.
        
       | glutamate wrote:
       | Love the idea, but i am having a hard time finding out what the
       | code looks like. Where can i see the code for spawn, receive and
       | send?
       | 
       | > ... a command-line utility designed to simplify the process of
       | generating boilerplate code for your project based on the Ergo
       | Framework
       | 
       | Why is there any boiler plate code at all? Why isn't hello world
       | just a five line programme that spawns and sends hello world
       | somewhere 5 times?
        
         | whalesalad wrote:
         | I was looking for the same thing. A project like this really
         | needs an `examples/` directory with a few projects to sink your
         | teeth into.
         | 
         | I've been thinking for years that if a project existed like
         | this for Python it would take over the world. Golang is close,
         | I guess.
        
           | nvarsj wrote:
           | It's right there. https://github.com/ergo-services/examples
           | 
           | It looks like a close copy of Erlang APIs, albeit with the
           | usual golang language limitations and corresponding
           | boilerplate and some additional stuff.
           | 
           | Most interesting to me is it has integration with actual
           | Erlang processes. That could fill a nice gap as Erlang lacks
           | in some areas like media processing - so you could use this
           | to handle those kind of CPU bound / native tasks.
           | func (a *actorA) HandleMessage(from gen.PID, message any)
           | error {         switch message.(type) {           case
           | doCallLocal:             local := gen.Atom("b")
           | a.Log().Info("making request to local process %s", local)
           | if result, err := a.Call(local, MyRequest{MyString: "abc"});
           | err == nil {               a.Log().Info("received result from
           | local process %s: %#v", local, result)             } else {
           | a.Log().Error("call local process failed: %s", err)
           | }             a.SendAfter(a.PID(), doCallRemote{},
           | time.Second)             return nil
        
             | whalesalad wrote:
             | I can't tell which syntax is worse: erlang or golang.
        
             | Fire-Dragon-DoL wrote:
             | This is crazy, so you could use it side-by-side to an
             | elixir process
        
             | fidotron wrote:
             | Honestly for Erlang integration just use NIFs or an actual
             | network connection.
             | 
             | That golang is a mess, and demonstrates just what a huge
             | conceptual gap there really is between the two. Erlang
             | relies on many tricks to end up greater than the sum of its
             | parts, like how receiving messages is actually pattern
             | matching over a mailbox, and using a tail recursive pattern
             | to return the next handling state. You could conceivably do
             | that in golang syntax but it will be horrible and
             | absolutely not play nicely with the golang runtime.
        
               | jerf wrote:
               | The ideal situation for this sort of code is to basically
               | treat it as marshalling code, which is often ugly by its
               | nature, and have the "payload" processing be
               | significantly larger than this, so it gets lost as just a
               | bit of "cost of doing business" but is not the bulk of
               | the code base.
               | 
               | Writing safe NIFs has a certain intrinsic amount of
               | complication. Farming off some intensive work to what is
               | actually a Go node (or any other kind of node, this isn't
               | specific to Go) is somewhat safer, and while there is the
               | caveat of getting the data into your non-BEAM process up
               | front, once the data is there you're quite free.
               | 
               | Then again, I think the better answer is just to make
               | some sort of normal server call rather than trying to
               | wrap the service code into the BEAM cluster. There's not
               | actually a lot of compelling reasons to be "in" the
               | cluster like that. If anything it's the wrong direction,
               | you want to _reduce_ your dependency on the BEAM cluster
               | as your message bus.
               | 
               | (Non-BEAM nodes have no need to copy using tail recursion
               | to process the next state. That's a detail of BEAM, not
               | an absolute requirement. Pattern matching out of the
               | mailbox is a requirement... a degenerate network service
               | that is pure request/response might be able to
               | coincidentally ignore it but it would be necessary in
               | general.)
        
               | amdsn wrote:
               | NIFs have the downside of potentially bringing down the
               | VM don't they? It's definitely true that the glue code
               | can be a pain and may involve warping the foreign code
               | into having a piece that plays along nicely with what
               | erlang expects. I messed around with making erlang code
               | and python code communicate using erl_interface and the
               | code to handle messages pretty much devolved into "have a
               | running middleman process that invokes erl_interface
               | utilities in python via a cffi wrapper, then finally call
               | your actual python code." Some library may exist or could
               | be written to help with that, but it's a lot when you
               | just wanna invoke some function elsewhere. I also have
               | not tried using port drivers, the experience may be a bit
               | different there.
        
               | toast0 wrote:
               | Yeah, NIFs are dynamically linked into the running VM,
               | and generally speaking, if you load a binary library, you
               | can do whatever, including crashing the VM.
               | 
               | BEAM has 4 ways to closely integrate with native code:
               | NIFs, linked in ports, OS process ports
               | (fork/ecommunicate over a pipe), and foreign nodes (C
               | Nodes). You can also integrate through normal networking
               | or pipes too. Everything has plusses and minusses.
        
             | hinkley wrote:
             | I don't know Go well, but this API would surely piss Alan
             | Kay off.
             | 
             | Why a function that takes an Actor instead of each Actor
             | being a type that implements a receive function? There's so
             | much Feature Envy (Refactoring, Fowler) in this example.
             | There is no world where having one function handle three
             | kinds of actors makes any sense. This is designed for
             | footguns.
             | 
             | I also doubt very much that the Log() call is helping
             | anything. Surely lathe API is thin enough to inline a that
             | child.
        
               | justasitsounds wrote:
               | > I don't know Go well, but this API would surely piss
               | Alan Kay off.
               | 
               | > Why a function that takes an Actor instead of each
               | Actor being a type that implements a receive function?
               | 
               | That function is a method with receiver type ` _Actor` -
               | IE `_ Actor` implements this HandleMessage function.
               | 
               | Granted it is exactly equivalent to ``` func
               | HandleMessage(a *Actor, from gen.PID, message any) error
               | { ... } ```
               | 
               | But I'm happy sticking with composition over inheritance
        
         | mjlee wrote:
         | The examples are in another repo:
         | 
         | https://github.com/ergo-services/examples
        
         | weego wrote:
         | This was a constant source of confusion for me with Akka. They
         | seemed almost proud of how much boilerplate and how many weird
         | implicit conversions were exposed to the developer.
        
           | theflyinghorse wrote:
           | That's just Scala community in general.
        
       | that_guy_iain wrote:
       | I love the name. It's just so good.
        
       | Fire-Dragon-DoL wrote:
       | How does the supervision tree look like? My major problem with go
       | are goroutines bringing down tge whole software
        
       | yellowapple wrote:
       | Are the actors/processes preemptively scheduled? I've seen many
       | projects like this over the years, and that's the usual missing
       | feature.
        
         | Fire-Dragon-DoL wrote:
         | Goroutines are preemptive scheduled, so the answer is yes
        
       | __MatrixMan__ wrote:
       | I like that this is a thing, I'm trying to decide how excited I
       | should be about it...
       | 
       | How equivalent are BEAM processes and goroutines?
        
         | kbolino wrote:
         | Most significantly: goroutines are not addressable. This means
         | that, from application code, you cannot send messages directly
         | to a goroutine [1], you cannot link two goroutines, you cannot
         | terminate a goroutine from another goroutine, you cannot
         | directly watch a goroutine's exit [2], and there's nothing akin
         | to the process dictionary (there are no "goroutine-local
         | variables").
         | 
         | [1]: you usually use channels instead
         | 
         | [2]: you usually use sync.WaitGroup (or similar, e.g.
         | errgroup.Group) instead
        
           | Zambyte wrote:
           | Notably channels differ from actor mailboxes in that they
           | have a capacity, and can block the sender. Mailboxes cannot
           | block the sender, and they do not have a capacity in theory.
        
             | kbolino wrote:
             | The ordinary send operation blocks if the channel is full
             | (or has no buffer), but you can send (or receive) without
             | ever blocking, if you can accept that the message is
             | dropped instead. It's not the most ergonomic operation,
             | though:                   var sent bool         select {
             | case channel<-message:                 sent = true
             | default:                 sent = false         }
        
             | brynb wrote:
             | with that said it's quite easy to write an equivalent- http
             | s://github.com/redwood/redwood/blob/develop/utils/mailbo...
        
         | jerf wrote:
         | My rather extensive earlier answer:
         | https://news.ycombinator.com/item?id=34564228
        
           | __MatrixMan__ wrote:
           | And a fantastic one at that. Thanks.
        
       | debo_ wrote:
       | It has a great name, ergo I support it. /joke
        
       | samuell wrote:
       | This is super exciting!
       | 
       | I've written before about how I think the more FBP-style
       | concurrency of Go and the message passing one in Erlang
       | complement each other as much as streaming DNA processing inside
       | the cell and fire and forget cell to cell signaling between cells
       | do in biology:
       | 
       | https://livesys.se/posts/flowbased-vs-erlang-message-passing...
       | 
       | The FBP/CSP-style in Go being more suited for high performance
       | streaming operations inside apps and compute nodes (or cells),
       | while the message passing mechanism shines over more unreliable
       | channels such as over the network (or between cells).
       | 
       | Ergo seems like it might allow both of these mechanisms to be
       | implemented in the same language, which should be a big deal.
        
       | nahuel0x wrote:
       | Three big differences in comparison with Erlang: 1- Cannot
       | externally kill a process (yes, ergo process have a Kill method
       | but the process will be in a "zombie" state until the current
       | message handlers returns... maybe stuck forever) 2- No hot code
       | reloading. 3- No per-process GC.
        
         | didip wrote:
         | (Not affiliated with Ergo) I have been watching Ergo repo for a
         | while now and you wrote my observation concisely.
         | 
         | A while back, I tried to solve no.2 on your list by having a
         | dynamically expanding and shrinking go-routines. POC here:
         | https://github.com/didip/laborunion. It is meant to be used in-
         | conjunction with config library & application object that's
         | update-able over the wire. This idea is good enough to hot-
         | reload a small IoT/metrics agent, but I never got around to
         | truly solve the problem completely.
        
           | mportela wrote:
           | Great project name, though
        
         | throwaway894345 wrote:
         | I've never written any Erlang before--why do I care about per-
         | process GC?
        
           | com wrote:
           | Much lighter impact on system performance that world-GC.
           | Simpler algorithms as well, so lower risk that you'll ever
           | get performance regressions - or worse.
        
           | victorbjorklund wrote:
           | More consistent performance. No stopping the whole world.
        
             | throwaway894345 wrote:
             | That makes sense. I wonder how important this is versus Go,
             | considering Go has a sub-millisecond GC even without per-
             | process GC? (Go also makes much more use of the stack which
             | might be sort of analogous to per-process GC?)
        
               | weatherlight wrote:
               | This can cause GC to become "bursty."
               | 
               | BeamVM languages complete side step this problem all
               | together.
        
           | davisp wrote:
           | Also, for anyone not completely familiar with Erlang's
           | terminology, the translation of "per process garbage
           | collection" to Go would be "per goroutine garbage
           | collection". As mentioned in a sibling comment, this allows
           | Erlang style garbage collection to avoid pausing the entire
           | operating system process when running garbage collectin.
        
             | whizzter wrote:
             | Per-process GC is an optimization similar to nurseries in
             | regular collectors, esp any object that has been sent in a
             | message must be visible globally (yes there could be small
             | object optimizations but that would increase sender
             | complexity).
             | 
             | Also an overlooked part here is that the global Erlang GC
             | is easier to parallellize and/or keep incremental since it
             | won't have object cycles sans PID's (that probably have
             | special handling anyhow).
             | 
             | TlDr; GC's become way harder as soon as you have cyclic
             | objects, Erlang avoids it and thus parts of it being good
             | is more about Erlang being "simple".
        
               | hinkley wrote:
               | Erlang is much more likely for GC overhead to grow sub-
               | linearly, because more logic means more isolates
               | (processes) rather than more state, more deeply nested,
               | in the existing processes. Say at the square root of
               | total data.
        
               | toast0 wrote:
               | Erlang avoids object cycles because it's impossible to
               | make an old term point to a new one; data is immutable,
               | so new terms can only referenece previous terms. This
               | means the GC doesn't have to consider cycles and keeps
               | things simple.
               | 
               | But that's separate from per process GC. Per process GC
               | is possible because processes don't share memory[1], so
               | each process can compact its own memory without
               | coordination with other processes. GC becomes stop the
               | process, not stop the world, and it's effectively
               | preemptable, so one process doing a lot of GC will not
               | block other processes from getting cpu time.
               | 
               | Also, per process GC enables a pattern where a well tuned
               | short lived process is spawned to do some work, then die,
               | and all its garbage can be thrown away without a complex
               | collection. With shared GC, it can be harder to avoid the
               | impact of short lived tasks on the overall system.
               | 
               | [1] yes yes, shared refcounted binaries, which are
               | allocated separately from process memory.
        
               | neonsunset wrote:
               | > GC's become way harder as soon as you have cyclic
               | objects
               | 
               | This may be true only for some implementations. _Good_ GC
               | implementations operate on the concept of object graph
               | roots. Whether the graph has cyclic references or not is
               | irrelevant as the GC scans the relevant memory linearly.
               | As long as the graph is unrooted, such GC implementations
               | are able to still easily collect it (or, to be more
               | precise, ignore it - the generational moving GCs the cost
               | is the live objects that need to be relocated to an older
               | /tenured generation).
        
           | petejodo wrote:
           | One reason also is I believe it has something to do with
           | fault tolerance even at a hardware level. A process has its
           | data isolated somewhere in memory, if something happens to
           | that memory, the process will crash next time it runs and
           | starts causing supervisors to start attempting to recover the
           | system
        
             | hinkley wrote:
             | Forced decoupling between tasks is part of the deal here.
             | Each task can fail separately because it only affects other
             | tasks through messages.
        
         | neonsunset wrote:
         | No per-process GC (still _very_ configurable) but for hot-
         | reload, if you don 't mind a completely different language,
         | there are Akka.net and Orleans:
         | 
         | https://github.com/akkadotnet/akka.net
         | 
         | https://github.com/dotnet/orleans
        
           | sbuttgereit wrote:
           | Of course... if you don't mind a completely different
           | language and runtime stack... there's always Erlang & Elixir!
        
             | neonsunset wrote:
             | This is true, but they come with a different set of
             | tradeoffs w.r.t ecosystem, tooling and performance (which
             | turns into per-node efficiency in this case). There is also
             | a matter of comfort and team/personal preferences.
        
         | throwawaymaths wrote:
         | Most beam developers will tell you they don't use hot code
         | reloading, but if you're an elixir/phoenix (especially live
         | view) you are using hot code reloading affordances in dev,
         | though it's not the full suite of capabilities
        
       | mervz wrote:
       | Just use Elixir... it's a better language overall.
        
         | mr90210 wrote:
         | People didn't get the gist of your comment.
         | 
         | Here is another way of looking at it:
         | 
         | One could write mobile apps and mobile games in Go, but should
         | you really?
        
       | cynicalsecurity wrote:
       | Go is not the correct language for this. This must be written in
       | a language that support manually freeing variables from the
       | memory. Go is a garbage-collected programming language.
       | 
       | Go is also not supposed to be used in a complicated way. Its use
       | cases must remain simple.
        
         | mr90210 wrote:
         | I have never written anything in Erlang, but if my system
         | requires some of Erlang's built-in features, I am picking
         | Erlang or Elixir.
        
       | giancarlostoro wrote:
       | This is strange, I don't know when it changed, but this project
       | used to be compatible with Erlang directly...[0] Used to support
       | the OTP. I guess their needs changed? You literally could write
       | Go code that would talk to Erlang itself, I'm not sure why this
       | has changed or where that functionality has gone but the page
       | aside from a repository tag no longer mentions OTP. I was looking
       | at this project for that very goal a month or two back.
       | 
       | [0]: https://news.ycombinator.com/item?id=34559409
        
         | AlphaWeaver wrote:
         | It's still present, they've moved it to a separate repository
         | that's now commercially licensed.
        
       | Dowwie wrote:
       | can I remote into a Ergo runtime and inspect state?
        
       | rad_gruchalski wrote:
       | My go to actor framework for golang has always been
       | https://github.com/asynkron/protoactor-go. It seems that both
       | protoactor and ergo are heavily influenced by Erlang. Why would
       | one select ergo over protoactor?
        
       | throwawaymaths wrote:
       | Can you link processes? goroutines? in ergo?
        
       ___________________________________________________________________
       (page generated 2024-09-12 23:00 UTC)