Packages

The cloud pieces of a dandelion service: clustering, cache, ordered queue, durable pub/sub

Current section

Files

Jump to
dandelion README.md
Raw

README.md

# dandelion
**Simplicity with resilience and joy.**
A whole cloud in one app — web server, workers, queue, pub/sub, cache, cron,
a live frontend — as one Elixir release, laid out the way a Go service is. Run
one copy on one VM; run 3 or 10 and they join into one system. Only the load
balancer, Postgres and file storage stay outside.
This package is the **library** half: the cloud pieces that are the same in
every dandelion project. The other half, the generator (`dandelion_new`),
writes a project that uses them.
```sh
mix archive.install hex dandelion_new
mix dandelion.new my_app # a service that already depends on this library
```
## Proof
Every claim is recorded, not just written down — [`evidence/`](https://github.com/danceinthefake/dandelion/tree/main/evidence)
has a log, a GIF and a page for each, and [what is *not* proven](https://github.com/danceinthefake/dandelion/blob/main/evidence/LIMITS.md).
**An order made on one node appears in a browser on another** (two browsers,
two different nodes, real Chromium):
![two browsers on two different nodes; an order made in one shows in the other](https://raw.githubusercontent.com/danceinthefake/dandelion/main/evidence/02-live-feed/live-feed.gif)
**A job left by a killed node is run again by another**, and payment events
keep their order while three nodes race for them — with negative controls that
show the same checks **fail** when the feature is switched off:
![the cluster proof](https://raw.githubusercontent.com/danceinthefake/dandelion/main/evidence/04-ordered-queue/ordered-queue.gif)
## The library
Thin over [Oban](https://hex.pm/packages/oban), [Cachex](https://hex.pm/packages/cachex),
`Phoenix.PubSub` and [libcluster](https://hex.pm/packages/libcluster) — not a
new API on top of them. You need Postgres and a `Phoenix.PubSub`.
```elixir
def deps do
[{:dandelion, "~> 0.1"}]
end
```
| Module | What it is | Replaces |
|---|---|---|
| `Dandelion.Cluster` | nodes find each other through Postgres (`NOTIFY`) | Consul, Kubernetes DNS |
| `Dandelion.Cache` | per-node memory cache, cleared on every node | Redis as a cache |
| `Dandelion.Queue` | the Oban setup: queues, lifeline, pruner, crontab | Cloud Tasks, Cloud Scheduler |
| `Dandelion.Queue.Ordered` | order per key, across nodes | SQS FIFO, Kafka partition keys |
| `Dandelion.PubSub` | a topic → one Oban job per subscriber, in your transaction | Google Pub/Sub, Kafka topics |
| `Dandelion.Migration` | the database index the ordered queue needs | — |
```elixir
# application.ex
children = [
MyApp.Repo,
{Dandelion.Cluster, otp_app: :my_app, repo: MyApp.Repo},
{Phoenix.PubSub, name: MyApp.Broadcast},
{Dandelion.Cache, pubsub: MyApp.Broadcast},
{Oban, Dandelion.Queue.config(otp_app: :my_app, repo: MyApp.Repo, crontab: MyApp.Cron.schedule())}
]
# a migration, after Oban's
def up, do: Dandelion.Migration.up()
def down, do: Dandelion.Migration.down()
# in your code
Dandelion.Cache.fetch({:product, sku}, fn -> Repo.get(Product, sku) end)
Dandelion.Cache.delete({:product, sku}) # after the commit, on every node
Repo.transact(fn ->
{:ok, order} = insert_order(params)
Dandelion.PubSub.publish(%{"order.created" => [SendConfirmation]}, "order.created", %{"id" => order.id})
{:ok, order} # saved together, or not at all
end)
Dandelion.Queue.Ordered.insert(MyWorker.new(args), "order:42") # then, in perform/1:
# with :ok <- Dandelion.Queue.Ordered.turn(job), do: ...
```
Each module's docs say what it promises and what it doesn't. The rules behind
them: **Postgres decides; memory only makes things faster.** Anything that must
survive a crash or happen exactly once goes through the database; the cache and
live broadcasts may be lost or briefly stale.
Nodes trust each other fully — anyone holding the Erlang cookie can run code on
every node. Keep them on a private network.
## The rest of the project
On [GitHub](https://github.com/danceinthefake/dandelion):
- [`example/`](https://github.com/danceinthefake/dandelion/tree/main/example) —
a service using all of it: `lib/platform/` (what every app runs on) and
`lib/app/shop/` (one domain laid out as handlers → services → repos →
models), a Vue + blessing-ui frontend, a release Docker image.
[`deploy/`](https://github.com/danceinthefake/dandelion/tree/main/example/deploy)
runs it as 3 nodes and **proves** the claims (an order made on one node shows
up on another; a killed node's jobs are run again by the others; payment
events keep their order while three nodes race for them).
- [`phrasebook/`](https://github.com/danceinthefake/dandelion/tree/main/phrasebook) —
for each Go habit, the Elixir way: 12 pages on the service, 5 on the rest of
the cloud.
- [`DESIGN.md`](https://github.com/danceinthefake/dandelion/blob/main/DESIGN.md) —
why it's built this way.
Name: *dandelion* — the plainest flower there is; it grows through cracks in
concrete and comes back every time you pull it; its seeds scatter on the wind —
one flower becoming many, the way a service starts single and grows into a
cluster.
## License
MIT — see [LICENSE](https://github.com/danceinthefake/dandelion/blob/main/LICENSE).