15 ms·
Writing Rust the Elixir Way
- anonymousDan 6y agoCan your runtime handle non rust wasm code?
- bkolobara 6y agoYes! I only provide a higher-level library for Rust, but you can use the lower-level host functions from any WASM code if you run it on Lunatic.
- bkolobara 6y agoAuthor here! I will take some time to answer any questions.
- codetrotter 6y agoLunatic seems very enticing to me. I remembered it, but not its name, when I read the intro to this blog post and my question was going to be if you’d tried it and what you thought of it but then a couple of paragraphs further into the blog post I learned that you are the author of Lunatic :P But instead I would like to ask about the future of Lunatic. What is you vision for it? Like, is it a hobby project that you are doing for fun or is it an endeavor that you intend for to be powering big systems in production? Furthermore, how will you fund the development of Lunatic? And where will other contributors come from? Will they be people scratching their own itches only or will you hire people to work on it with a common roadmap?
- bkolobara 6y agoI started working on Lunatic around a year ago in my free time. Until now I just wanted to see if it was even technically possible to build such a system. Now that I have the foundation in place and you can build some "demo" apps, like a a tcp server, I'm starting to explore options for funding. From this point on I think that the progress is going to be much faster, especially if others find it useful enough to contribute to it.
- mwcampbell 6y agoHere's a possible funding idea: See if you can get one of the big cloud providers or CDNs to notice the project, so they can hire you to build something that goes head to head with Cloudflare Workers.
- daxfohl 6y agoWe're hiring.... https://careers.microsoft.com/us/en/job/915502/Senior-Software-Engineer https://careers.microsoft.com/us/en/job/915502/Senior-Softwa... And definitely looking at doing something like this next year.
- scns 6y agoMaybe you can apply at https://prototypefund.de/en/ https://prototypefund.de/en/
- nahuel0x 6y agoI think the Lunatic architecture can enable shared memory regions between processes and between processes and host in addition to the erlang-like shared nothing approach. That would be an advantage over (non-NIF) erlang for some use-cases. Do you plan something like that? Can you easily map a data structure in Rust? (I think is doable between WASM process but not sure about between WASM and native host). Another question: What about sending code between process? Like sending a fun between Erlang process. IMHO this architecture has the potential to go beyond BEAM, good work!
- bkolobara 6y agoThank you! This is something I want to support and there is some WIP code for it. Currently I'm only waiting on Wasmtime (the WASM runtime used by Lunatic) to get support for shared memory. Regarding the question about sending code, this also can be implemented. Wasm allows you to share function indexes (pointers) and dynamically call them.
- caust1c 6y agoComing from Go which introduced me to the concept of lightweight threads, I really miss goroutines and Go's concurrency model with channels/select/cancellation/scheduling. However, targeting WASM seems like a pretty huge compromise to doing this natively in Rust. Conversely, it looks like what you're trying to do wouldn't currently be possible without the WASM/WASI stack. Curious what your thoughts are about not implementing it as a Rust runtime target? Did you investigate this and why did you rule it out if so?
- bkolobara 6y agoI don't see it as a compromise. Using Wasm allows for some really nice properties, like compiling once and running on any operating system and CPU architecture. Lunatic JIT compiles this code either way to machine code so the performance hit should not be too big. When you say "implementing it as a Rust runtime target", I assume you mean targeting x86/64. In this case it would be impossible to provide fault tolerance. The sandboxing capability of Wasm is necessary to separate one processes memory from another.
- nahuel0x 6y agoNote, the Go model falls short against the Erlang/Lunatic one. You can't externally kill a goroutine at arbitrary points (you need to cancel them with a cancel chan / context with manual checking). You can't prioritize a goroutine over others. You can't serialize a goroutine state and send it over the network. You can't fork a goroutine. You can't sandbox a goroutine. Etc. Using WASM is a tradeoff, but WASM is very fast, surely faster than BEAM process.
- didibus 6y ago> You can't externally kill a goroutine at arbitrary points Why not? Isn't that just an artifact of Go's implementation, or is there fundamentally something in CSP that prevents this?
- skybrian 6y agoIt's essentially cultural. A fundamental assumption of the Go language, implementation, libraries, and community is that goroutines share memory. You don't write concurrent Go code assuming that a Goroutine could arbitrarily disappear, so breaking this assumption will break things. Lunatic is not like that: > Each Lunatic process gets their own heap, stack and syscalls. Similarly, Rust code that uses threads and/or async isn't going to work in Lunatic (or at least won't get the advantages of its concurrency model) without being rewritten using Lunatic's concurrency primitives. The concurrency model is more like JavaScript workers or Dart isolates, though hopefully lighter weight. I'm guessing that the Rust language might work well because move semantics assumes that values that aren't pinned can be moved, and that's a better fit for transmitting messages between processes. But there will probably be a lot of crates that you can't use with Lunatic. If it became popular, it would be forking the community at a pretty low level. You'd have some crates that only work with Lunatic and others that use async or threads.
- ibraheemdev 6y ago> working with async Rust is not as simple as writing regular Rust code Working with async Rust is very simple if you are not writing futures yourself. Tokio and async-std provide api's that mirror std except for the fact that you have to add `.await` to the end of the operation and add the `async` keyword to your function. With proc-macros, `async main` is just a matter of adding a small annotation to the top of the function. Async functions in traits are blocked due to compiler limitations, but `async-trait` makes it easy. What part of async Rust is more complicated than synchronous Rust? > and it just doesn't provide you the same features as Elixir Processes do. Can you explain this? How is tokio's `task` different then elixir processes? From the tokio docs: > Another name for this general pattern is green threads. If you are familiar with Go's goroutines, Kotlin's coroutines, or Erlang's processes, you can think of Tokio's tasks as something similar.
- romanoderoma 6y agoErlang processes are not green threads. Green threads can share memory while Erlang processes cannot, they are strictly forbidden to do it. Also Erlang scheduler is highly optimized to work as a soft real-time platform, so they never run for infinite amount of time, they never block and never (at least that's the goal) bring down the entire application, the worst thing that can happen is that everything slows down but it's still functional and responsive. I don't know about Tokio.
- ibraheemdev 6y ago> Erlang processes are not green threads. Green threads can share memory while Erlang processes cannot, they are strictly forbidden to do it. So message passing is the only way to communicate between proccesses? I guess that makes sense with elixir being a fp language. This was not clear in the article: > Lunatic takes the same approach as Go, Erlang and the earlier implementation of Rust based on green threads.
- dnautics 6y ago> So message passing is the only way to communicate between proccesses? There are escape hatches. Obviously, you can do something really heavy like open a network port or open a file descriptor, but it also provides you with use ets, which is basically "really fast redis for intra-vm stuff" and you can transact over shared memory if you drop down to FFI.
- jimbokun 6y agoWhat do you see as the advantage of using these Rust + WASM running on Lunatic processes, versus developing in Elixir?
- bkolobara 6y agoUsing C bindings without fear would be a big one. If you use NIFs in Elixir you may bring down the whole VM with a segmentation fault. While Lunatic limits this failure to only the affected process. WASM was designed to be run inside the browser so the sandboxing capabilities are much stronger. Another would be raw compute power, WASM comes close to native in many cases.
- nahuel0x 6y agoOther advantages over BEAM: Static typing, multiple languages with a WASM target available, live migration of process to the browser.
- dwoot 6y agoRecently started my journey with Rust having worked with Elixir in production for about two years, but I keep an ear out on Rust and Elixir/Erlang development My question: are you familiar with the Lumen project? https://github.com/lumen/lumen#about https://github.com/lumen/lumen#about. Both projects appear to have some overlap. Secondly, what, if any, will be the primary differentiators here? (I've not looked at either projects in too much detail) One of its creators, Paul Schoenfelder (Bitwalker), you're probably familiar with as he's authored a few popular libraries in Elixir and the other core developer, Luke Imhoff, is ensuring that WASM is taking players like Elixir/Erlang into account being a part of one of the organizations or committees if I recall correctly
- asimpletune 6y agoOne question that I have is at what point do you call something a VM? Also, I haven’t looked at your code, so maybe this is not understanding things correctly, but wrt targeting wasm, could your work ever make it into the actual rust runtime?
- steveklabnik 6y ago> into the actual rust runtime? Rust has the same level of runtime as C, with no plans to expand it. So, regardless of how awesome this project is, this is unlikely. (Notably, there is no "Rust runtime" for async, you have to bring your own if you want to do it.)
- faitswulff 6y agoSo must the `crashing_c_function()` in your example be compiled to WASM before it can be used in Lunatic? Another comment elsewhere asked: > Well then that is not comparable to NIFs at all. In fact it is an extremely misleading example...A lot of the time you can not compile the C code to wasm and what do you do then? How do you interface with OS libraries? https://news.ycombinator.com/item?id=25255955 https://news.ycombinator.com/item?id=25255955
- bkolobara 6y agoYes, it must be compiled to Wasm to work.
- toast0 6y agoThis is a bit out of left field, and perhaps more to motivate others to try, but... Have you considered making this a port driver for BEAM? Then you could call some function from Elixir to launch a wasm actor (that happens to be written in rust)? Your BEAM would still be imperiled if the Lunatic? layer violates conventions, of course; but it may (or may not) be simpler than reinventing the rest of OTP?
- qw3rty01 6y agoIs there a reason you used threads in the rust example instead of tasks? I think it would have been more useful to compare against proper rust async concurrency: I ran 200k sleep tasks over 12 threads using `async-std` and each thread only used up ~30% cpu (~60% on `tokio`), and <10% with 20k tasks. What is the proper way to call `Process::sleep` in your example? I don't see it in the lunatic crate or documentation, and I can't compare results without running the same test in lunatic. Edit: I guess async rust is mentioned, but it doesn't really explain in detail what lunatic is solving that async rust doesn't provide, besides a vague "more simple" and "extra features," which the example doesn't really show.
- ema 6y agoI'm extremely surprised that creating 2k threads makes mac os reboot. Sure that's a lot for one application but not a totally crazy amount.
- bkolobara 6y agoI believe that this is related to the operating system being overwhelmed with waking up every 100ms on 2k threads. This example is not that great though. Depending on the OS and CPU you should be able to run much higher amount of threads.
- toast0 6y agoI'm not too surprised. mac os isn't tuned out of the box for high loads, and in some areas really can't be (there's no synflood protection, since they forked FreeBSD tcp from months before that was added, for example) Not that 2k threads is really that high; but it's probably high enough to break something.
- DerDangDerDang 6y agoYou’d expect it to kill the offending user space process rather than the OS though, right?
- toast0 6y agoIf you hit an explicit limit, I'd expect the thread spawn to fail, and most processes to panic if thread spawning failed, sure. But if you run below the explicit limit, but above the implicit limit of whatever continues to work, it's not surprising to me that the OS just freaked out. You could report it to Apple, but their reporting process is pretty opaque, especially if you're not a registered developer (because why would you be, if you're just using mac os because you like the desktop environment, or whatever). Who knows if they'll fix it, but it's not worth making a big deal over, because you weren't really wanting to run 2k threads on mac os anyway, because it would suck, even if it did work. From the other message on the thread; it looks like too many threads is causing a watchdog timer to fail, leading to the panic.
- rapsey 6y agoWait why does calling a crashing c function not crash the process?
- nahuel0x 6y agothe C code is compiled to WASM bytecodes, they are interpreted in a WASM VM. If the C function crash, it does it inside the VM, so the crash is contained.
- rapsey 6y agoWell then that is not comparable to NIFs at all. In fact it is an extremely misleading example.
- kibwen 6y agoWhy is it misleading? If BEAM could isolate crashes in NIFs, it absolutely would. Either way the point is that you are able to leverage C code (though in Rust's case the point is just to leverage existing code, since you don't need to drop into C for performance).
- rapsey 6y agoBecause you are not actually calling a native compiled C lib you are calling wasm code. A lot of the time you can not compile the C code to wasm and what do you do then? How do you interface with OS libraries?
- kibwen 6y agoYou may be focusing too closely on the C aspect; the point of the demonstration in the OP is to show that Lunatic can gracefully sandbox and interoperate with any code that can be compiled to WASM, of which C code is just one example. Ideally you wouldn't need to access any OS libs from within the sandbox (indeed, much of the point of the sandbox is to provide alternatives to OS facilities), and even if you did you could still access those libs in an un-sandboxed form from within your ordinary Rust code (and yes, crashing at that point would take down the process, but it's still strictly better to have an option to run C code that doesn't take down the process when crashing).
- jowi-dev 6y agoSo I am very fascinated by both languages and work in Elixir professionally, with a pretty minimal knowledge of Rust. I was just wondering what the motivation for creating this project was rather than just writing the application in Elixir with NIFs in Rust whenever higher performance is needed?
- bkolobara 6y agoIf a NIF fails it takes down the whole Erlang VM, so you loose all guarantees that make Elixir great once you resort to NIFs. There are many other gotchas, like if you spend too much time inside the NIF it will affect the stability of your whole system. This is eliminated by some design choices taken by Lunatic. But as I mentioned in the blog, if you can use Elixir/Erlang do it! It's a great language and runtime.
- jowi-dev 6y agoVery interesting and good to know! I've been keeping a closer eye on Rust lately as its beginning to look more and more like it would be foolish to ignore. Rust and Go have really made it hard to conceptualize where Elixir fits into the dev world these days. Go just seems to be a faster Elixir in a lot of ways. I'll keep an eye on this project and if you need any help - feel free to tag me on github (same username as this one)
- dnautics 6y ago> Go just seems to be a faster Elixir in a lot of ways If you haven't worked with both, you just don't know. The developer experience in go is nowhere near the developer experience in Elixir. It's just so much easier to write scalable code in Elixir than Go. Sure it's not as fast, but it's not god-awful slow (speedwise, you're doing better than python django, for example), and for 90% of people shopping for what elixir and go offer, the network is the bottleneck.
- jowi-dev 6y agoI've only worked in Elixir. I think I just recently experienced some Go "fomo" because it seems to be more or less attempting to solve the same problem. I can't quite justify moving to Go from Elixir + Rust, but I'm always interested in hearing the community at-large take. Thanks for the response!
- akhilcacharya 6y agoTo me, this looks extremely promising from a performance and developer ergonomics point of view and a fantastic use case for WASM and WASI - only limiting factor seems to be the rollout for WASI networking support.
- toast0 6y ago> Now you can write regular blocking code, but the executor will take care of moving your process off the execution thread if you are waiting, so you never block a thread. This is super important and awesome. It's a big part of what makes Erlang simpler to write code for than other highly concurrent environments.
- ibraheemdev 6y agoasync-std has been doing this for a while. The runtime detects blocking tasks automatically and offloads them to a seperate thread. EDIT: The automatic blocking detection of async-std was abandoned after further discussion
- bkolobara 6y agoThere is an important difference here. In Lunatic all syscalls are non-blocking, they are not just offloaded to a separate thread. However, they look like regular "blocking" functions from the developer's perspective. Under the hood Lunatic uses async-wormhole[0] to make this work. [0]: https://github.com/bkolobara/async-wormhole https://github.com/bkolobara/async-wormhole
- mwcampbell 6y agoDid that actually go into a release? The last I heard was that they tried it on a branch about a year ago and then abandoned it.
- ibraheemdev 6y agoI though it had been merged but it looks like they remove the auto blocking detection and merge a revised version of the runtime [1]. 1: https://github.com/async-rs/async-std/pull/733 https://github.com/async-rs/async-std/pull/733
- anthony_doan 6y agoYeah it because the Erlang VM have a preemptive scheduler and each process have a mailbox which gets a designated amount of time to access cpu. You can do a for(1) for loop and it won't hog the cpu because it get premptive. The problem is when you have large amount of problematic mails in the mailbox....
- IshKebab 6y ago> but I will call them processes to stay close to Elixir's naming convention. That seems like an extremely confusing mistake given that there is already a very closely related concept called a "process"? I would consider calling them "workers" or "isolates" since unless I'm mistaken, you have basically recreated WebWorkers (AKA isolates in V8/Dart)? Presumably this also means you can't share memory? Very neat idea anyway!
- FpUser 6y ago>"Even though the threads don't do anything, just running this on my MacBook forces it to reboot after a few seconds. This makes it impractical to have massive concurrency with threads." That is a problem of your MacBook. I can run thousands of threads when testing some servers sustainably and with no problems on my Windows and Linux laptop, never mind desktop and real servers. So it is pretty much practical. Whether it makes sense pretty much depends on what are you doing in particular. Your conclusion means zilch without context.
- lalaithion 6y agoRunning this test on my Linux desktop takes 9.9 GB of virtual memory (although very little is actually resident) and 42% of my CPU. So, it doesn't crash my computer, but this seems like it's way too much resource usage.
- FpUser 6y agoReserved stack space that can be adjusted
- FpUser 6y agoForgot to add, for 2000 threads repeatedly doing some very short calculation (added it for fun) and sleeping 100ms in a loop: CPU consumption is around 2% (fluctuates between 1 and 2.6). RAM used 36MB. So do not know what is wrong with your system / or maybe whatever language/library being used.
- Dylan16807 6y agoSomeone else in the HN comments reproduced it so it's definitely an OS problem.
- FpUser 6y agoI did not mean it to be hardware problem. Of course it is OS related.
- didibus 6y agoLove Erlang and Elixir and Beams concurrency model is quite interesting. This Rust variant seems very interesting as well. But I wanted to discuss this part: > In most cases it's enough to use a thread pool, but this approach fails once the number of concurrent tasks outgrows the number of threads in the pool I think something has been lost from all the dialogue about concurrency. The only way the number of concurrent task can outgrow the number of threads is when you're handling IO. What I see rarely discussed here is forms of non-blocking IO and why those haven't become the norm. Why are we all trying to work around models that involve blocking on IO? I feel I almost never hear about using non-blocking IO as an alternative? For all other tasks which involve computation, thread pools are needed to improve throughput and performance, only threads can be executed in parallel. Yes you can still multiplex many more small compute task over many threads, if you wanted a really responsive but overall slower program, that could be an option, but I think most people going for a concurrent approach aren't doing so for that, but really just as a way to leverage their IO cards to the fullest. So my question is, what's wrong with having a thread pool combined with non-blocking IO ?
- mwcampbell 6y ago> What I see rarely discussed here is forms of non-blocking IO and why those haven't become the norm. Why are we all trying to work around models that involve blocking on IO? I feel I almost never hear about using non-blocking IO as an alternative? Maybe I don't understand what you mean by non-blocking I/O, because it seems to me that it is the norm in many environments, for better or worse. Async/await syntax is now available in C#, JavaScript, Python, Rust, and possibly others. And before that we had callbacks in JavaScript and in Python frameworks like Twisted and Tornado. So it seems to me that non-blocking I/O has been widely used and discussed for quite a while now.
- didibus 6y agoI think I don't often hear it in the context of C# and Rust maybe, or what I mean is, in the context of a multi-threaded environment, so in combination with a Thread Pool. Say you used the thread pool model. And you receive 100 requests, and you have a thread pool of 50. Your first 50 requests go to your 50 threads, and the other 50 get queued up. After any of the running 50 requests make non-blocking IO, they release the thread back to the pool, and they yield themselves into an IO waiting queue. Then the scheduler takes from either the request queue or the IO waiting queue prioritizing one over the other as see fit. If any of the queues are full, then the system blocks and waits, dropping requests that'd come in during that time. Or some similar models. Is this just async/await that I described?
- yetkin 6y agoHi, can you please explain the Wasm part? Is there any particular reason to use Wasm? I don't know wasm but does it use a specific threading/concurrency mechanism in it?
- kibwen 6y agoLooking at the Lunatic readme: https://github.com/lunatic-lang/lunatic#architecture https://github.com/lunatic-lang/lunatic#architecture "Lunatic treats WebAssembly instances as actors. Using WebAssembly in this context allows us to have per actor sandboxing and removes some of the most common drawbacks in other actor implementations."
- yetkin 6y agoAnd it would be something more lightweight then a process/thread/green thread I presume?
- stepbeek 6y agoIs this the perfect Hacker News title?
- radicalriddler 6y agoThey should've included the WASM part in it
- bausano_michael 6y agoThe submitted link does no longer for me, here's a link to the original article: https://dev.to/bkolobara/writing-rust-the-elixir-way-2lm8 https://dev.to/bkolobara/writing-rust-the-elixir-way-2lm8
- ibraheemdev 6y agoBastion [1] is also inspired by Erlang and provides many of the features described in this post. Although it does not compile to Wasm, it provides: - Message-based communication - Runtime fault-tolerance - a lean mesh of actor system. - A Supervision system that makes it easy to kill or restart subprocesses Does anyone have experience with Bastion or other Rust actor systems? 1: https://bastion.rs/ https://bastion.rs/
- ecnahc515 6y agoWhat's the performance like? I think this would be really interesting if comparable to something like Go.