[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)