{"id":13822563,"url":"https://github.com/irrustible/async-backplane","last_synced_at":"2025-04-09T20:06:33.291Z","repository":{"id":62438231,"uuid":"254808595","full_name":"irrustible/async-backplane","owner":"irrustible","description":"Simple, Erlang-inspired fault-tolerance framework for Rust Futures.","archived":false,"fork":false,"pushed_at":"2021-07-07T11:15:19.000Z","size":179,"stargazers_count":129,"open_issues_count":0,"forks_count":5,"subscribers_count":15,"default_branch":"main","last_synced_at":"2025-04-09T20:06:26.840Z","etag":null,"topics":["async","async-await","asynchronous","erlang-otp","fault-detection","fault-tolerance","recovery","reliability","reliability-backplane","rust"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/irrustible.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-04-11T06:37:52.000Z","updated_at":"2025-03-20T04:39:44.000Z","dependencies_parsed_at":"2022-11-01T21:50:19.659Z","dependency_job_id":null,"html_url":"https://github.com/irrustible/async-backplane","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/irrustible%2Fasync-backplane","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/irrustible%2Fasync-backplane/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/irrustible%2Fasync-backplane/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/irrustible%2Fasync-backplane/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/irrustible","download_url":"https://codeload.github.com/irrustible/async-backplane/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248103872,"owners_count":21048245,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["async","async-await","asynchronous","erlang-otp","fault-detection","fault-tolerance","recovery","reliability","reliability-backplane","rust"],"created_at":"2024-08-04T08:02:06.414Z","updated_at":"2025-04-09T20:06:33.255Z","avatar_url":"https://github.com/irrustible.png","language":"Rust","readme":"# async-backplane\n\n[![License](https://img.shields.io/crates/l/async-backplane.svg)](https://github.com/irrustible/async-backplane/blob/main/LICENSE)\n[![Package](https://img.shields.io/crates/v/async-backplane.svg)](https://crates.io/crates/async-backplane)\n[![Documentation](https://docs.rs/async-backplane/badge.svg)](https://docs.rs/async-backplane)\n\nEasy, Erlang-inspired fault-tolerance framework for Rust Futures.\n\nFeatures:\n\n* The secrets of Erlang's legendary reliability.\n* Idiomatic Rust API with low-level control.\n* Simple. Easy to learn and use.\n* Plays nicely with the existing Futures Ecosystem\n* Uses no unstable features or unsafe code.\n* High performance and (relatively) low memory\n* Lightweight: ~600 lines of code, 6 deps, fresh build in seconds. \n* No `Box\u003cdyn Any\u003e`, LOL.\n\n## Status.\n\nEverything is believed to work correctly, but we're still too new to\nbe sure. The API may change slightly before 1.0, but nothing major, I\nhope.\n\nPlease note that this is a more general purpose, lower-level tool than\nmost libraries that claim to be inspired by erlang. It is the plan\nthat other libraries will provide a higher level experience. I'm\nworking on some, which will be coming soon:\n\n* [async-supervisor](https://github.com/irrustible/async-supervisor)\n\n## Guide\n\n### Introduction\n\nThe Backplane (that's a fancy word for 'motherboard') is a dynamic\nmesh of `Device`s - owned objects representing a backplane\npresence. On dropping a `Device` or calling its `disconnect()` method,\nother Devices that have chosen to hear about it will be notified.\n\nAll erlang-style reliability springs from this one capability to be\nnotified of the failure of your dependencies. It is the lower-level\ntool upon which more advanced concepts such as the famous supervisors\nare built.\n\nCreating a `Device` is easy:\n\n```rust\nuse async_backplane::Device;\n\nfn device() -\u003e Device { Device::new() }\n```\n\nWhat is a `Device`? What does having a presence in the backplane mean?\n\n* We maintain a list of `Devices` to notify.\n* When we `disconnect`, we will notify those Devices.\n\nThere are two triggers for a disconnect:\n\n* The `Device` is dropped.\n* The `Device`'s `disconnect()` method is called.\n\nOnce a `Device` has disconnected, you can no longer use it. No more\nlinking, no more messaging, it is done.\n\nThe `Device` is a futures `Stream` and can be polled for `Message`s. A\nmessage is one of two things:\n\n* A request to shut down with the `DeviceID` of the requestor.\n* A notification that another `Device` has disconnected. This contains\n  the `DeviceID` of the disconnecting Device and an `Option\u003cFault\u003e`\n  describing the nature of the disconnect.\n  \nHere's an example of polling it in an async fn:\n\n```rust\nuse async_backplane::{Device, Message};\nuse futures_lite::StreamExt; // for `.next()` on Stream\n\nasync fn next_message(device: \u0026mut Device) -\u003e Option\u003cMessage\u003e {\n    device.next().await\n}\n```\n\nThis is much more useful if there is something to listen for, which is\nwhere linking comes in!\n\n### Linking\n\nLinking is how we configure Devices to notify each other when they\ndisconnect (drop or have `.disconnect()` called on them). There are\nthree types of link mode (`LinkMode`):\n\n* `Monitor` - be notified when the other Device disconnects.\n* `Notify` - notify the other Device when this Device disconnects.\n* `Peer` - both notify each other when they disconnect.\n\nLinking is pretty easy if you have a pair of Devices (such as when\nyou're spawning a new Device):\n\n```rust\nuse async_backplane::Device;\n\n// `l` will be notified when `r` disconnects\nfn monitor(l: \u0026Device, r: \u0026Device) { l.link(r, LinkMode::Monitor); }\n\n// `r` will be notified when `l` disconnects\nfn notify(l: \u0026Device, r: \u0026Device) { l.link(r, LinkMode::Notify); }\n\n// `l` will be notified when `r` disconnects\n// `r` will be notified when `l` disconnects\nfn peer(l: \u0026Device, r: \u0026Device) { l.link(r, LinkMode::Peer); }\n```\n\nNow we have something to listen for, let's keep restarting a failing\ntask for all eternity:\n\n```rust\n\nuse async_backplane::*;\nuse futures_lite::StreamExt; // for `.next()` on Stream\nuse smol::Task; // just a small and simple futures executor\n\nasync fn never_stop\u003cF: Fn(Device)\u003e(mut device: Device, spawn: F) {\n    loop { /// We want to go forever\n        let d = Device::new();\n        device.link(\u0026d, LinkMode::Monitor);\n        spawn(d);\n        while let Some(message) = device.next().await {\n            match message {\n                Message::Shutdown(id) =\u003e (), // ignore!\n                Message::Disconnected(_id, _fault) =\u003e { break; } // restart!\n            }\n        }\n    }\n}\n\n/// This is quite obviously not going to succeed. Maybe yours should!\nfn failing_task(device: Device) {\n    smol::Task::spawn(async {\n      device.disconnect(Some(Fault::Error(())))\n    }).detach();\n}\n\nfn main() {\n    never_stop(failing_task)\n}\n```\n\nIn a sense, we have just written our first supervisor! A new crate,\n[async-supervisor](https://github.com/irrustible/async-supervisor)\nis coming soon with erlang-style supervisors.\n\n### Managed devices\n\nExciting as all this low level control over how we respond to exits\nis, if we take the erlang model seriously, we generally leave this to\nsupervisors, and most of our tasks are *not* supervisors.\n\nNon-supervisor tasks just want to get on with their work. That means\nif any Device they are monitoring disconnects with a `Fault`, they too\nwill want to disconnect with a `Fault`. In this sense, links are a\n*dependency graph* between `Device`s (which are proxies for the\ncomputations using those `Device`s).\n\nWe call this extremely common scenario *managed mode*. It can be\naccessed through the `Device.manage()` method:\n\n```rust\nuse async_backplane::*;\nuse smol::Task;\n\nfn example() {\n    let device = Device::new();\n    Task::spawn(async move {\n        device.manage(async { Ok(()) }); // Succeed!\n    }).detach();\n}\n```\n\nThere are three logical steps here:\n* Creating the Device (`Device::new()`).\n* Spawning a Future on the executor (`Task::spawn(...).detach()`).\n* In the spawned Future, putting the Device into managed mode\n  with an async block to execute (`device.manage(async { Ok(()) })`).\n\nThe async block you provide to `Device.manage()` should return a\n`Result` of some kind. If you return `Ok`, the Device will be\nconsidered to have completed without fault. If you return `Err`, the\nDevice will be considered to have faulted.\n\nManaged devices will run until the first of:\n* The provided future/async block returning a result.\n* The provided future/async block unwind panicking.\n* A Device sending us a message:\n  * On receiving a shutdown request, complete successfully.\n  * On receiving a disconnect notification that is fatal, fault.\n\nBy calling `.manage()`, you are giving up ownership of the Device\npermanently. When one of the above happens, any Devices that are\nmonitoring us will be notified.\n\nThe `manage()` method returns a `Result\u003cT, Crash\u003cC\u003e\u003e` where `T` and\n`C` are the success and error types of the `Result\u003cT,C\u003e` returned by\nthe async block. `Crash` is just an enum with an arm for each kind of\nfailure. It contains detailed information about what went wrong, whereas\nany *notification* of our disconnection contains only basic information.\n\nI'm still trying to work out what to do with crashes. I don't want\nthis library to be too opinionated or to bloat the dependency tree too\nmuch. Maybe I'll do an opinionated library that uses this one, or\nmaybe you'll just create your own `manage_panic()` function in each\nproject and use that? Suggestions gratefully received!\n\n### Dynamic link topologies\n\nOften, we will want to use `Device.manage()` to get the automatic\nmanagement behaviour, but we'll also want to link with new Devices as\npart of that work But `manage()` takes ownership of the `Device`\npermanently, so what do we do?\n\nA `Line` is a cloneable reference to a `Device` in the style of an\n`Arc` (and indeed, contains one). The gotcha is that because the\n`Line` is non-owning, the `Device` it references could have\ndisconnected by the time you try to use it, so linking may fail:\n\n```rust\nuse async_backplane::*;\n\nfn example() {\n    let a = Device::new();\n    let b = Device::new();\n    let line = b.line();\n    a.link_line(line, LinkMode::Monitor) // suspiciously like `.link()`...\n      .unwrap(); // b clearly did not disconnect yet\n    // ... spawn both ...\n}\n```\n\nNote that `link_line()` consumes the `Line`. This is because\ninternally, the list of notifiable `Device`s is actually a list of\n`Line`, so we avoid a clone in the case you no longer  need the `Line`.\n\nYou can link between `Lines` directly as well, since `Line` also has a\n`link_line()` method:\n\n```rust\nuse async_backplane::*;\n\nfn demo() {\n    let a = Device::new();\n    let b = Device::new();\n    let c = Device::new();\n    let c2 = c.line();\n    let d = Device::new();\n    let d2 = d.line();\n    a.link(\u0026b, LinkMode::Peer); // Device-Device link\n    b.link_line(c2, LinkMode::Peer).unwrap(); // Device-Line link\n    c2.link_line(d2, LinkMode::Peer).unwrap(); // Line-Line link\n    // ... now go spawn them all ...\n}\n```\n\nAny time you will want dynamically link while you are using\n`Device.manage()`, you should create a `Line` first.\n\n#### A note of caution on dynamic topologies\n\nOnce you have linked with something through a `Line`, you should only\nunlink it through the `Line`. Device-to-Device linkage is fast because\nit avoids the work that would make it handle this case correctly. In\ngeneral, you should only link or unlink with `Device`s when you know\nyou have not previously linked with the corresponding `Line`s.\n\n### Differences from Erlang/OTP\n\nWhile I am very heavily inspired by Erlang and the OTP principles,\nthere's a bit of an impedance mismatch Rust and Erlang, in particular\nwhen it comes to ownership versus garbage collection. backplane is\nthus an adaptation of the principles that \"feels right\" for Rust.\n\nWhere it's ended up after a few months of R+D is as a lower level tool\nthat tries not to be too pushy and opinionated and is extremely small.\n\nHere are some of the more striking differences\n\n#### Separation between Device and logic\n\nIn erlang, when you wish to spawn a process, you provide a 0-arity\nfunction. By default, it works essentially like `Device.manage()`\nwithout the transfer of ownership.\n\nIn backplane, I do not want to force my choice of executor or\nexecution policy on you, so creating a `Device` is totally independent\nof spawning the Future that will use it, out of necessity.\n\nThis means that while most code will called `Device.manage()`, you\nhave full freedom to implement whatever logic you want and to store\nthe `Device` where you want.\n\n#### Separation between Device and Mailbox\n\nIn erlang, all messages sent to a process go through the same channel\n(the mailbox). In a sense, a Device does have a mailbox, but it is of\nstrictly limited utility. `Device`s do not handle any messages other\nthan `Message`, whereas erlang messages may be anything. In order to\nexchange general messages with the tasks using the `Device`s, you\nwould need to e.g. open an `async-channel` channel.\n\n### FAQ\n\n#### Why erlang?\n\nYour author has been an Elixir programmer by profession for the last\nfew years and has come to appreciate deeply the principles underlying\nthe reliability of Erlang, upon which Elixir is based. Above all, what\nI value is the simplicity. The entire system is simple enough to be\nable to reason about at scale.\n\n#### Haven't other people already tried this? Why reinvent the wheel?\n\nMuch of it is taste. I don't think any existing solutions really\ncapture the essence of what erlang reliability is about, or give a\nfeel for its essential beauty. People seem to get too tied up in\nactors and supervision and focus less on the fundamentals.\n\nExisting solutions also tend to be large, complex things that are\ndifficult to learn and reason out and pull in a lot of\ndependencies. The whole point of erlang to me is that it makes\nconcurrency and dependency so simple, you can reason about them at\nscale. But I fear we're drifting back to discussing taste.\n\nI also gave [a specific comparison with\nbastion](https://www.reddit.com/r/rust/comments/i1was2/asyncbackplane_simple_erlanginspired/g02ztn0/)\non reddit by request. Just my opinion, others are available.\n\n### Library pairing recommendations\n\nThese work great alongside `async-backplane`:\n\n* [async-oneshot](https://github.com/irrustible/async-oneshot) - a\n  fast, small, full-featured, no-std compatible oneshot channel\n  library.\n* [async-channel](https://github.com/stjepang/async-channel/) - great\n  all-purpose async-aware MPMC channel.\n* [smol](https://github.com/stjepang/smol/) - small, high-performance\n  multithreaded futures executor.\n  \nThese will, when they're finished:\n\n* [async-supervisor](https://github.com/irrustible/async-supervisor) -\n  erlang-style supervisors for async-backplane.\n* [async-oneshot-local](https://github.com/irrustible/async-oneshot) -\n  the single-threaded partner to `async-oneshot`.\n\n## Performance\n\nI didn't spend terribly long developing the benchmarks, you should\nconduct your own if it really matters.\n\nHere are numbers from my Ryzen 3900X:\n\n```\n     Running target/release/deps/device-90347ed9496e0aaa\n\nrunning 11 tests\ntest create_destroy              ... bench:         231 ns/iter (+/- 3)\ntest device_monitor_drop         ... bench:         526 ns/iter (+/- 11)\ntest device_monitor_drop_notify  ... bench:         604 ns/iter (+/- 12)\ntest device_monitor_error_notify ... bench:         632 ns/iter (+/- 9)\ntest device_peer_drop_notify     ... bench:         659 ns/iter (+/- 10)\ntest device_peer_error_notify    ... bench:         671 ns/iter (+/- 10)\ntest line_monitor_drop           ... bench:         634 ns/iter (+/- 11)\ntest line_monitor_drop_notify    ... bench:         687 ns/iter (+/- 11)\ntest line_monitor_error_notify   ... bench:         717 ns/iter (+/- 8)\ntest line_peer_drop_notify       ... bench:         764 ns/iter (+/- 14)\ntest line_peer_error_notify      ... bench:         778 ns/iter (+/- 9)\n\ntest result: ok. 0 passed; 0 failed; 0 ignored; 11 measured; 0 filtered out\n\n     Running target/release/deps/line-750db620e6752c99\n\nrunning 6 tests\ntest create_destroy            ... bench:           8 ns/iter (+/- 0)\ntest line_monitor_drop         ... bench:         637 ns/iter (+/- 8)\ntest line_monitor_drop_notify  ... bench:         670 ns/iter (+/- 11)\ntest line_monitor_error_notify ... bench:         698 ns/iter (+/- 8)\ntest line_peer_drop_notify     ... bench:         843 ns/iter (+/- 7)\ntest line_peer_error_notify    ... bench:         917 ns/iter (+/- 11)\n```\n\nAnd it still performs reasonably on my old 2015 macbook pro:\n\n```\n     Running target/release/deps/device-8add01b9803770b5\n\nrunning 11 tests\ntest create_destroy              ... bench:         212 ns/iter (+/- 9)\ntest device_monitor_drop         ... bench:         585 ns/iter (+/- 64)\ntest device_monitor_drop_notify  ... bench:         771 ns/iter (+/- 39)\ntest device_monitor_error_notify ... bench:         798 ns/iter (+/- 39)\ntest device_peer_drop_notify     ... bench:         964 ns/iter (+/- 40)\ntest device_peer_error_notify    ... bench:         941 ns/iter (+/- 304)\ntest line_monitor_drop           ... bench:         805 ns/iter (+/- 48)\ntest line_monitor_drop_notify    ... bench:         975 ns/iter (+/- 48)\ntest line_monitor_error_notify   ... bench:         993 ns/iter (+/- 55)\ntest line_peer_drop_notify       ... bench:       1,090 ns/iter (+/- 62)\ntest line_peer_error_notify      ... bench:       1,181 ns/iter (+/- 65)\n\ntest result: ok. 0 passed; 0 failed; 0 ignored; 11 measured; 0 filtered out\n\n     Running target/release/deps/line-c87021ef05fddd66\n\nrunning 6 tests\ntest create_destroy            ... bench:          13 ns/iter (+/- 4)\ntest line_monitor_drop         ... bench:         793 ns/iter (+/- 51)\ntest line_monitor_drop_notify  ... bench:         968 ns/iter (+/- 357)\ntest line_monitor_error_notify ... bench:       1,018 ns/iter (+/- 54)\ntest line_peer_drop_notify     ... bench:       1,343 ns/iter (+/- 70)\ntest line_peer_error_notify    ... bench:       1,370 ns/iter (+/- 77)\n```\n\nNote that when linking, it is cheaper to use a Device than a Line, that is:\n\n* `device.link()` is fastest.\n* `device.link_line()` is slightly more expensive.\n* `line.link_line()` is slightly more expensive still.\n\nIf performance really matters, always link Device to Device. Also\nspend some time optimising this library, because we didn't yet.\n\n## Forthcoming work\n\n* no_std support.\n* Actors. Maybe.\n\n## Changelog\n\n### v0.1.1\n\n* Fixed `Crash.is_completed`\n\nI also fixed the clippy lints and rearranged a tiny bit of code.\n\n## Copyright and License\n\nCopyright (c) 2020 James Laver, async-backplane Contributors\n\nThis Source Code Form is subject to the terms of the Mozilla Public\nLicense, v. 2.0. If a copy of the MPL was not distributed with this\nfile, You can obtain one at http://mozilla.org/MPL/2.0/.\n\n","funding_links":[],"categories":["Rust"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Firrustible%2Fasync-backplane","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Firrustible%2Fasync-backplane","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Firrustible%2Fasync-backplane/lists"}