A LiveView Is a Process
fly.io
fly.io
With LiveView, it works like this:
- user pushes a button to render the PDF
- the LV process calls `Task.async(fn -> do_work({data, self.pid()}))` so the worker process has the PID of the LiveView process itself
- when the job completes on a different thread, the worker sends an Elixir message using the standard and highly-flexible built-in mechanism with `send()` to the PID of the LV
- the LV gets the message and updates the UI and/or initiates a download of the rendered PDF automatically
It is so simple to get this workflow going. It's super efficient. Perfect for low- to mid-range load on a web app. Perfect for one-person development.
From the docs: "Alternatively, if you spawn a task [via async] inside a GenServer, then the GenServer will automatically await for you and call GenServer.handle_info/2 with the task response..."
Also if you prefer sequencing the response manually the way you're doing, Task.start_link is probably more appropriate.
Note that the way you're doing it now your liveview's message queue will leak with unhandled Task.async responses and down messages (probably not a big deal given your problem domain). If you do go the Task.start_link way, you do get two way binding (if the task dies the liveview will restart itself).
I think it’s a testament to the power of the platform that the obvious dumb solution worked so well for me. Lol
Background worker systems like Oban or sidekiq are where you want to run your async work imo. You can still do a pub/sub type notifier model to bubble up the job’s completion, or poll. Polling is perfectly acceptable for most apps and traffic levels and is simpler to mentally grok compared to websockets and persistent connections. Websockets require a central broker to pass messages through when you scale past one process and with elixir the options are the redis pubsub adapter, or for container based deployments or those wanting to use the built in distributed erlang functions, libcluster. More complexity. More points of failure. More potential centralized bottlenecks. More things to mentally model when debugging. More dev and prod drift. Just poll, until you can’t scale your web servers any more. It’s just an http request. It’s already implemented if you’re serving web traffic.
It’s not clear based on what you wrote but are the pdf’s contents being passed in the complete message? That sounds like an OOM termination that’s hard asf to track down just waiting to happen. PDF should go into blob storage somewhere and the task should bubble up a complete message, which then is used to signal the retrieval and proxying of the file to the user. If you have to background the pdf generation, it’s slow. If it’s slow to generate a document, it’s large. You don’t want that to be pushed through memory because your system stability is then coupled to not only the contents of a document that could exceed the memory of the system, but also is at the mercy of how many users/requests are all generating pdfs at the same time, and of course you’ll have that one guy spamming the “generate” button and queuing up 300 jobs because we didn’t need indicators on the UI and who adds uniqueness constraints to internal tools ;) Disks and file systems are for passing very large messages on a system
The user destroys their first liveview instance and gets a new liveview instance. You may not have properly set up bidirectional links so the destruction of this first liveview kills the first background job, or it may run to completion and be discarded. This might be exactly the semantics the user wants. If the user expects state to stick around that would need to be implemented separately.
> If you have to background the pdf generation, it’s slow. If it’s slow to generate a document, it’s large.
I'm not sure it's quite that slow. It's too slow to freeze the entire UI while it generates. But it's fast enough blocking a http response until it finishes often works. A simple system for transient background processing like Task.async might be appropriate.
> Disks and file systems are for passing very large messages on a system
We don't know what the type of the response message is. It could well be a filename in a temporary dirextory. That would be the simplest way to implement a background task that shells out.
I believe that this is one of the big advantages of SSE vs. WebSockets. Unlike websockets, an SSE connection is just an HTTP request, just one that has a particular format and is conventionally long-lived. You don't need a broker and you can probably implement it yourself on top of whatever HTTP server library that you're already using. And it already contains functionality for dealing with network unreliability [1].
I've often thought that it would be really good to have better support for SSE in HTTP caching HTTP proxies. Then you could have nginx for example sitting infront of your SSE endpoints serving multiple clients with a single SSE stream to your backend. Just based on the caching headers and a little knowledge of `Last-Event-ID`.
[1]: You can encode any state you require to resume the connection into Last-Event-ID.
Why? Because people think they know what LiveView is, but they don't, because the pretenders out there (LiveWire, StimulusReflex and all the rest) are poor imitations.
Don't sleep on LiveView.
But that doesn't change the fact that they don't reproduce half of what LiveView can do.
They didn't have to call themselves "LiveView for Ruby!"
I meant it more as a PSA: if you've tried any of these other projects, you still owe it to yourself to give LiveView a try!
built my startup on elixir/phoenix. while there could certainly be a better selection of libraries, its not that hard to roll your own and elixir has straight up features that you can't replicate in most other ecosystems
Calling them poor imitations is disingenuous and spitting on open work of many people.
Biggest Liveview app i know is fly.io dashboard and the issues it has from ux standpoint are very similar to hotwire apps.
Actually Basecamp and Hey.com are a lot bigger apps than fly.io dashboard and they are doing just fine.
You don't have to jump to Elixir to experience similar approach when you know rails/laravel already.
One would think that the imitations would come after the original not before.
I’ve been a part of porting the Phoenix LiveView Protocol to both Javascript (https://LiveViewJS.com) and Go (https://github.com/canopyclimate/golive) backends and supported a friend that is porting it to Python.
Ironically I think taking LiveViews outside of Elixir could actually make it easier for folks to adopt Elixir-based LiveViews in the future.
Because outside of Go (and maybe Rust) it will be very difficult to actually do what Elixir does, with the same level of concurrency and fault tolerance, etc, with the same ergonomics.
Ruby/Rails/StimulusReflex does something very similar, but kind of falls over unless you replace ActionCable with AnyCable which is written in Go.
So now, you have to support Go and Ruby runtimes. Some developer who decided that the above was a nightmare to work with, __might__ start a new project in Elixir, to get something that actually does what Elixir does instead of just mimicking it.
I'm not sure I buy this, personally, but I can understand the argument.
It's just as difficult in Go and Rust. BEAM, the Erlang VM that powers Elixir, is not just about lightweight processes. It's also about guarantees that neither Go nor Rust provide. E.g. to do what Elixir does you need robust process supervision trees. You can imitate, but not replicate those in other languages.
Then in the vast majority of cases we wouldn't run kubernetes clusters running docker containers for one-off tasks and single services. Or "serverless functions".
Unfortunately even the otherwise very smart folks of Go and Rust only see "ah, lightweight threads, got you". That's why, for example, you get irrecoverable panics in both languages and awkward attempts to patch over them in later versions.
Meanwhile Armstrong's thesis "Making reliable distributed software in the presence of hardware/software errors" is over thirty years old now.
Should it?
> otherwise very smart folks of Go and Rust
Ahh, everyone else is wrong. Gotcha. RabbitMQ is life(LOL), god save the BEAM!
Erlang VM guarantees that [1]
- all processes are isolated. Fatal error in one process will not affect other processes. Crucial: it will not affect the VM.
- If you set up process monitoring, and that process dies for whatever reason the monitor will be notified, with the actual reason. This lets the monitor (the supervising process) decide what to do next: restart, do nothing, stop entirely etc.
- Other relevant things like processes suspended when waiting for messages, or running out of reductions (number of allocated calls and messages) so that other processes can be executed, a bunch optimizations in the VM about CPU caches etc.
So what you have is "kubernetes for processes, inside the VM".
And the reason you don't have supervision for either Go or Rust is simple. Both Go and Rust for some completely asinine reasons decided that an unhandled error should kill the entire running application, with no chance of recovery.
And later they both awkwardly bolted on a half-assed solution to catch panics. That's it. That's the extent of error handling and guarantees in those languages. Of course no one builds supervision trees there.
[1] Of course it's not a 100% guarantee, nothing is, but other languages don't have even that.
Erlang'd documentation used to have a section called "Do not program defensively" [1] This is known as "let it crash" philosophy. Or, better put, that Erlang has auto healing mechanisms [2]
When applied correctly, it significantly reduces the amount of boiler plate code you need to write because you're only concerned with the happy path in most of your code.
[1] A copy is available here: https://docs.jj1bdx.tokyo/Erlang_Programming_Rules.html#HDR1...
[2] Erlang has auto healing mechanisms
I'm in full agreement with you, but I'm not sure you need full robust process supervision trees to mimic what the BEAM does in the context of LiveView on a single machine.
I do want to say, I 100000% times prefer Elixir, it's tooling, ecosystem, web frameworks, easy of scaling vertically and horizontally, etc over Go or any other lang that probably do something analogous to LiveView via what ever concurrency primitives that language/runtime champions; Go with it's Communicating sequential processes(CSP) and Rust with the Ractor lib (https://github.com/slawlor/ractor).
I take a more classic web approach to LiveView which is to say I persist any important state right away. Like any multiple-step forms I would never store in server state, I'd persist each step. Of course that doesn't help if someone is halfway through filling out a form. If the socket disconnects then re-connects then you don't lose your work but not if you do a restart. I do wish there was a built-in way to store state on the client until it's submitted, we may get there, though!
The true value of LiveView for me is the simplicity of not having to write yourself a web API to interact with your own backend as well as the dead simple real-time features. If you don't need the latter, that's fine, but having worked on collaborative apps that was a giant pain is what led me to LiveView.
In practice, if you care about this aspect of user experience you may have to take some reasonably small steps to preserve state information in the client. Form state gets preserved automatically for most purposes, though there may be some forms that need to implement a call-back to preserve state, as explained in the docs: https://hexdocs.pm/phoenix_live_view/form-bindings.html#reco...
https://www.erlang.org/doc/design_principles/release_handlin...
Can't remember if/how that routing was accomplished in the BEAM (Erlang/Elixir VM). It's definitely possible but you might have to implement it yourself.
Besides scheduled maintenance, there’s all sorts of reasons why this might be needed: The user’s log-in expires, network issues, browser crashes, server hardware issues, deployment of a new version of your app…
“Hot upgrades” can be implemented if you wish to, but often this is just handled by asking the client to reload the page, establishing a new connection, because any data or preferences have been persisted.
Liveview makes no assumptions about the client connection or even stickiness of client to nodes in a cluster. If you store critical data in the liveview you could be in for some trouble. Always store important stuff into a recoverable, network aware datastore. The nice thing is that liveview is designed to be tolerant to client or client connection failures, you should design to be able to repopulate state sanely in the on_mount call.
While this sounds bad, think of it as isolating the client connection failure domain from the data failure domain, so you should organize your code in those domains respectively
If you want some sort of ephermal network state that lives on one node and not the database only (e.g. a game), the liveview should connect into that. If you want to store that in a genserver, a strategy like this is doable:
https://youtu.be/pQ0CvjAJXz4?t=1992
Bryan Hunter explains how they do it at HCA healthcare (not specifically LiveView process, but processes in general).
I'd think the dictionary approach is functionally equivalent in most respects, and could even imitate how crashes are handled in Elixir. The framework has to implement more code to do this (e.g. messages between processes might be something some frameworks don't bother to implement because its such a hassle, while its almost free in Elixir), but that doesn't matter at all to users of the framework if the features are there.
However, I'd argue that the key difference lies in the granularity of control and the inherent characteristics of the Erlang/Elixir process model, which is designed to excel in concurrent, real-time, and distributed systems.
For instance, in Elixir, every LiveView session is an isolated process that runs concurrently. This is native to the BEAM (Erlang's virtual machine), not an add-on. It's this built-in process model that allows LiveView to handle many users simultaneously, manage state efficiently, and push real-time updates, all while maintaining high performance.
Fault isolation, too, is a baked-in benefit of this process model. A crash in one process doesn't affect others. While it's certainly feasible to engineer fault isolation in a different paradigm, it's a baseline guarantee in Elixir, reducing a considerable amount of potential complexity from the get-go.
Elixir's supervision trees further provide a robust way of handling failures, automatically restarting processes when they crash. Implementing a similar mechanism outside of Elixir can be quite intricate and might not offer the same level of reliability.
Additionally, Elixir processes facilitate straightforward and efficient inter-process communication, making it easier to build distributed and concurrent systems. Other frameworks could implement similar features, but it might not be as effortless or as integral a part of the programming model as it is in Elixir.
So, while the dictionary approach can mimic some aspects of Elixir's processes, Elixir's model provides a more complete, integrated solution for building highly concurrent, real-time systems. The fact that many of these features are "almost free" in Elixir does matter, as it can reduce the complexity of application code, improve robustness, and increase developer productivity.
I wouldn't call it baked in, it's the entire purpose of the process model. The other benefits of the process model are secondary.
But most of us reading this article do not develop frameworks, we develop applications. And when we choose a framework to use for that, we look at the features it provides, not the features it leverages from lower-level frameworks.
You could simulate the same thing with a dictionary lookup scheme, but A) it would be far more complex, because of the lack of native message passing support, and B) the you'd miss out on the symmetries and interop, because the rest of your program wouldn't be using message passing.
The real best solution for this one major drawback I am aware of would be for Liveview to be able to generate some JS handling in their front-end widgets, more like Lambdera.
That, and/or run a BEAM on the user's machine via WASM so that they don't have to dirty up their hands in even the slightest amount of javascript lol! xD
Example: You click on an upvote button and it changes color, but there's 1 second of latency.
SPA: Color updates immediately, the "upvote pressed" http/websocket call to the server arrives one second later.
LiveView: liveview.js sends "upvote pressed" over the web socket, one second later the liveview process on the server gets the message, patches the dom and replies, one more second later the button changes color. Meanwhile the user has pressed the button 2 more times wondering why the color isn't updating.
There are phx-hooks (https://hexdocs.pm/phoenix_live_view/js-interop.html#client-...) to address this with small targeted bits of js where you can add event listeners but it can get messy quickly.
assign
ə-sīn′
transitive verb
1. To select for a duty or office; appoint: synonym: appoint.
2. To set apart for a particular purpose or place in a particular category; designate: synonym: allocate.
3. To give out as a task; allot.I've seen it start cropping up in some Fintech circles so I'm wondering if this language has some serious juice for concurrent data analysis or something.
That said, there are ways to extend the BEAM with C or Rust code to make those areas performant. I’ve written one such extension (called a NIF; forget what it stands for exactly) and it wasn’t all that hard.
It is not the right choice for CPU intensive tasks like graphics, HFT, etc. Some companies have used Rust to write native extensions for those kinds of problems. https://discord.com/blog/using-rust-to-scale-elixir-for-11-m...