├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── actors ├── Cargo.toml └── src │ ├── broker.rs │ ├── lib.rs │ ├── message_bus.rs │ ├── pool.rs │ └── pubsub.rs ├── banner.png ├── benches └── overhead.rs ├── cliff.toml ├── docs.json ├── docs ├── banner.png ├── core-concepts.mdx ├── core-concepts │ ├── actors.mdx │ ├── messages.mdx │ ├── replies.mdx │ ├── requests.mdx │ └── supervision.mdx ├── distributed-actors.mdx ├── distributed-actors │ ├── bootstrapping-actor-swarm.mdx │ ├── dialing-connecting-nodes.mdx │ ├── messaging-remote-actors.mdx │ └── registering-looking-up-actors.mdx ├── faq.mdx ├── farron.png ├── getting-started.mdx └── index.mdx ├── examples ├── basic.rs ├── broker.rs ├── forward.rs ├── macro.rs ├── manual_swarm.rs ├── message_bus.rs ├── pool.rs ├── pubsub.rs ├── pubsub_filter.rs ├── registry.rs ├── remote.rs └── stream.rs ├── macros ├── Cargo.toml └── src │ ├── derive_actor.rs │ ├── derive_remote_actor.rs │ ├── derive_reply.rs │ ├── lib.rs │ ├── messages.rs │ └── remote_message.rs ├── scripts ├── release-actors.sh ├── release-kameo.sh └── release-macros.sh └── src ├── actor.rs ├── actor ├── actor_ref.rs ├── id.rs ├── kind.rs └── spawn.rs ├── error.rs ├── lib.rs ├── mailbox.rs ├── message.rs ├── registry.rs ├── remote.rs ├── remote ├── _internal.rs └── swarm.rs ├── reply.rs ├── request.rs └── request ├── ask.rs └── tell.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [tqwewe] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug or issue with Kameo 4 | title: "[BUG] - " 5 | labels: bug 6 | assignees: tqwewe 7 | 8 | --- 9 | 10 | ## Bug Description 11 | A clear and concise description of what the bug is. 12 | 13 | ## Steps to Reproduce 14 | Please provide a set of steps to reproduce the issue: 15 | 1. Go to '...' 16 | 2. Run '...' 17 | 3. See error '...' 18 | 19 | ## Expected Behavior 20 | A clear and concise description of what you expected to happen. 21 | 22 | ## Actual Behavior 23 | What actually happened? 24 | 25 | ## Screenshots or Logs 26 | If applicable, add screenshots or logs to help explain your problem. 27 | 28 | ## Environment 29 | Please provide the following information about your setup: 30 | - OS: [e.g., macOS, Linux, Windows] 31 | - Rust version (`rustc --version`): [e.g., 1.55.0] 32 | - Version of Kameo (`cargo pkgid kameo`): [e.g., 0.11.0] 33 | 34 | ## Additional Context 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a new feature or enhancement for Kameo 4 | title: "[FEATURE] - " 5 | labels: enhancement 6 | assignees: tqwewe 7 | 8 | --- 9 | 10 | ## Feature Description 11 | A clear and concise description of the feature or enhancement you'd like to see. 12 | 13 | ## Motivation 14 | Explain why this feature is important or useful. What problem does it solve or what use case does it improve? 15 | 16 | ## Proposed Solution 17 | If you have an idea of how to implement this feature, please provide a high-level overview of the solution. 18 | 19 | ## Alternatives Considered 20 | Have you considered any alternative solutions? If so, please describe them. 21 | 22 | ## Additional Context 23 | Add any other context, screenshots, or examples that would help illustrate the feature. 24 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Cargo Build & Test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | RUSTFLAGS: "-Dwarnings" 10 | CARGO_TERM_VERBOSE: true 11 | 12 | jobs: 13 | build: 14 | name: Build - ${{ matrix.toolchain }} 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | toolchain: [stable, beta] 19 | features: ["--no-default-features", "--no-default-features --features macros", "--no-default-features --features tracing", "--no-default-features --features remote", "-p kameo_actors"] 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - name: Setup Rust toolchain 24 | uses: actions-rs/toolchain@v1 25 | with: 26 | toolchain: ${{ matrix.toolchain }} 27 | override: true 28 | 29 | - name: Cache dependencies 30 | uses: actions/cache@v3 31 | with: 32 | path: | 33 | ~/.cargo/registry 34 | ~/.cargo/git 35 | target 36 | key: ${{ runner.os }}-cargo-${{ matrix.toolchain }}-${{ hashFiles('**/Cargo.lock') }} 37 | restore-keys: ${{ runner.os }}-cargo-${{ matrix.toolchain }}- 38 | 39 | - name: Standard build 40 | run: cargo build ${{ matrix.features }} 41 | 42 | - name: Tokio unstable build 43 | if: "!contains(matrix.features, 'kameo_actors')" 44 | run: RUSTFLAGS="--cfg tokio_unstable" cargo build ${{ matrix.features }} 45 | 46 | test: 47 | name: Test - ${{ matrix.toolchain }} 48 | runs-on: ubuntu-latest 49 | strategy: 50 | matrix: 51 | toolchain: [stable, beta] 52 | features: ["--no-default-features", "--no-default-features --features macros", "--no-default-features --features tracing", "--no-default-features --features remote"] 53 | steps: 54 | - uses: actions/checkout@v4 55 | 56 | - name: Setup Rust toolchain 57 | uses: actions-rs/toolchain@v1 58 | with: 59 | toolchain: ${{ matrix.toolchain }} 60 | override: true 61 | 62 | - name: Cache dependencies 63 | uses: actions/cache@v3 64 | with: 65 | path: | 66 | ~/.cargo/registry 67 | ~/.cargo/git 68 | target 69 | key: ${{ runner.os }}-cargo-${{ matrix.toolchain }}-${{ hashFiles('**/Cargo.lock') }} 70 | restore-keys: ${{ runner.os }}-cargo-${{ matrix.toolchain }}- 71 | 72 | - name: Run tests 73 | run: cargo test ${{ matrix.features }} 74 | 75 | lint: 76 | name: Clippy - ${{ matrix.toolchain }} 77 | runs-on: ubuntu-latest 78 | strategy: 79 | matrix: 80 | toolchain: [stable] 81 | features: ["--no-default-features", "--no-default-features --features macros", "--no-default-features --features tracing", "--no-default-features --features remote"] 82 | steps: 83 | - uses: actions/checkout@v4 84 | 85 | - name: Setup Rust toolchain 86 | uses: actions-rs/toolchain@v1 87 | with: 88 | toolchain: ${{ matrix.toolchain }} 89 | override: true 90 | components: clippy 91 | 92 | - name: Cache dependencies 93 | uses: actions/cache@v3 94 | with: 95 | path: | 96 | ~/.cargo/registry 97 | ~/.cargo/git 98 | target 99 | key: ${{ runner.os }}-cargo-${{ matrix.toolchain }}-${{ hashFiles('**/Cargo.lock') }} 100 | restore-keys: ${{ runner.os }}-cargo-${{ matrix.toolchain }}- 101 | 102 | - name: Run clippy 103 | run: cargo clippy ${{ matrix.features }} 104 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | .direnv 4 | .DS_Store 5 | .makemd 6 | .obsidian 7 | .space 8 | .envrc 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Kameo 2 | 3 | Thank you for considering contributing to Kameo! We welcome all contributions, whether they are bug reports, new features, improvements to documentation, or anything else that can help improve the project. 4 | 5 | Please take a moment to review the guidelines below to ensure that your contributions are in line with the project’s standards. 6 | 7 | ## How to Contribute 8 | 9 | ### Reporting Bugs 10 | If you've found a bug or an issue with Kameo, we would appreciate if you could: 11 | 12 | 1. **Search Existing Issues**: Please check the [issues page](https://github.com/tqwewe/kameo/issues) to see if your bug has already been reported. If it has, feel free to add additional information as a comment. 13 | 2. **Create a New Issue**: If the issue hasn’t been reported, [create a new issue](https://github.com/tqwewe/kameo/issues/new) with the following details: 14 | - A clear title describing the bug. 15 | - Steps to reproduce the bug. 16 | - Expected behavior and what actually happened. 17 | - Relevant environment details (OS, Rust version, etc.). 18 | - Any other relevant information or screenshots. 19 | 20 | ### Suggesting Features or Enhancements 21 | We are always open to new ideas! If you have a feature or enhancement request, please: 22 | 23 | 1. **Search Existing Feature Requests**: Look through the [existing issues](https://github.com/tqwewe/kameo/issues) to see if someone has already suggested a similar feature. 24 | 2. **Open a New Feature Request**: If your idea hasn’t been suggested yet, [open a new issue](https://github.com/tqwewe/kameo/issues/new) describing: 25 | - The motivation for the feature. 26 | - How you envision the feature working. 27 | - Any additional context or examples that would be helpful. 28 | 29 | ### Pull Requests 30 | We welcome pull requests for bug fixes, new features, or improvements! Here’s how you can contribute: 31 | 32 | 1. **Fork the Repository**: Start by [forking the repository](https://github.com/tqwewe/kameo/fork) and cloning it locally. 33 | ```bash 34 | git clone https://github.com/tqwewe/kameo.git 35 | cd kameo 36 | ``` 37 | 2. **Create a New Branch**: Always create a new branch for your changes. 38 | ```bash 39 | git checkout -b my-feature-branch 40 | ``` 41 | 3. **Make Changes**: Implement your changes, making sure your code follows the project’s style and is well-documented. 42 | 4. **Test Your Changes**: If applicable, run the tests to ensure your changes don’t break anything. Add new tests if you’ve added a new feature or fixed a bug. 43 | ```bash 44 | cargo test 45 | ``` 46 | 5. **Use Conventional Commits**: We follow the [Conventional Commits](https://www.conventionalcommits.org) specification for commit messages. This ensures that commit messages are structured and informative. The format is: 47 | ```bash 48 | [optional scope]: 49 | 50 | [optional body] 51 | 52 | [optional footer(s)] 53 | ``` 54 | For example: 55 | ``` 56 | feat(actor): add support for remote actors 57 | fix(pubsub): resolve issue with message broadcasting 58 | ``` 59 | Common types include: 60 | - `feat`: A new feature. 61 | - `fix`: A bug fix. 62 | - `docs`: Documentation-only changes. 63 | - `style`: Changes that do not affect the meaning of the code (formatting, missing semicolons, etc.). 64 | - `refactor`: A code change that neither fixes a bug nor adds a feature. 65 | - `test`: Adding or modifying tests. 66 | 67 | If your changes introduce a **breaking change**, add an exclamation mark (`!`) after the type: 68 | ``` 69 | feat(actor)!: change remote actor messaging system 70 | ``` 71 | Breaking changes should also be mentioned in the commit body or footer for clarity. 72 | 73 | 6. **Commit and Push**: Commit your changes with a meaningful message and push your branch. 74 | ```bash 75 | git add . 76 | git commit -m "Description of changes" 77 | git push origin my-feature-branch 78 | ``` 79 | 7. **Create a Pull Request**: Go to the GitHub page of your forked repo and click on “New Pull Request”. In your pull request, provide a detailed explanation of your changes, referencing the issue if applicable. 80 | 81 | ### Code Guidelines 82 | 83 | - **Follow Rust’s Style**: Ensure your code follows Rust’s conventions. You can use `cargo fmt` to automatically format your code. 84 | - **Document Your Code**: Ensure that all public items (functions, structs, modules) have proper Rustdoc comments. 85 | - **Write Tests**: When adding new functionality, write tests to ensure the new code works as expected. 86 | 87 | ### Code of Conduct 88 | By contributing, you agree to abide by our [Code of Conduct](CODE_OF_CONDUCT.md). 89 | 90 | --- 91 | 92 | Thank you for contributing to Kameo! Your help makes this project better. 93 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [".", "actors", "macros"] 4 | 5 | [workspace.dependencies] 6 | kameo = { path = ".", version = "0.16.0" } 7 | 8 | futures = "0.3" 9 | tokio = "1.44" 10 | 11 | [profile.release] 12 | lto = true 13 | opt-level = 3 14 | codegen-units = 1 15 | 16 | [package.metadata.docs.rs] 17 | all-features = true 18 | 19 | [package] 20 | name = "kameo" 21 | description = "Fault-tolerant Async Actors Built on Tokio" 22 | version = "0.16.0" 23 | edition = "2021" 24 | readme = "README.md" 25 | repository = "https://github.com/tqwewe/kameo" 26 | license = "MIT OR Apache-2.0" 27 | categories = ["asynchronous", "concurrency", "rust-patterns"] 28 | keywords = ["actor", "tokio"] 29 | 30 | [features] 31 | default = ["macros", "tracing"] 32 | macros = ["dep:kameo_macros"] 33 | remote = ["dep:libp2p", "dep:libp2p-identity", "dep:linkme", "dep:rmp-serde"] 34 | tracing = ["dep:tracing", "tokio/tracing"] 35 | 36 | [dependencies] 37 | kameo_macros = { version = "^0.16.0", path = "./macros", optional = true } 38 | 39 | downcast-rs = "2.0.1" 40 | dyn-clone = "1.0" 41 | futures.workspace = true 42 | libp2p = { version = "0.55.0", features = ["cbor", "dns", "kad", "mdns", "macros", "quic", "request-response", "rsa", "serde", "tokio"], optional = true } 43 | libp2p-identity = { version = "0.2.9", features = ["rand", "rsa"], optional = true } 44 | linkme = { version= "0.3.28", optional = true } 45 | once_cell = "1.19" 46 | rmp-serde = { version = "1.3.0", optional = true } 47 | serde = { version = "1.0", features = ["derive"] } 48 | tokio = { workspace = true, features = ["macros", "rt", "sync", "time"] } 49 | tracing = { version = "0.1", optional = true } 50 | 51 | [dev-dependencies] 52 | criterion = { version = "0.5", features = ["async_tokio"] } 53 | kameo_actors = { path = "actors" } 54 | tokio = { version = "1", features = [ 55 | "macros", 56 | "rt", 57 | "rt-multi-thread", 58 | "sync", 59 | "time", 60 | ] } 61 | tokio-stream = { version = "0.1.15", features = ["time"] } 62 | tokio-test = "0.4.4" 63 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 64 | 65 | [[example]] 66 | name = "remote" 67 | required-features = ["remote"] 68 | 69 | [[example]] 70 | name = "manual_swarm" 71 | required-features = ["remote"] 72 | 73 | [[bench]] 74 | name = "overhead" 75 | harness = false 76 | 77 | [lints.rust] 78 | unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tokio_unstable)'] } 79 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [2024] [Ari Seyhun] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kameo 🎬 2 | 3 | [![Discord](https://img.shields.io/badge/Discord-5868e4?logo=discord&logoColor=white)](https://discord.gg/GMX4DV9fbk) 4 | [![Book](https://img.shields.io/badge/Book-0B0d0e?logo=mdbook)](https://docs.page/tqwewe/kameo) 5 | [![Sponsor](https://img.shields.io/badge/sponsor-ffffff?logo=githubsponsors)](https://github.com/sponsors/tqwewe) 6 | [![Crates.io Version](https://img.shields.io/crates/v/kameo)](https://crates.io/crates/kameo) 7 | [![docs.rs](https://img.shields.io/docsrs/kameo)](https://docs.rs/kameo) 8 | [![Crates.io Total Downloads](https://img.shields.io/crates/d/kameo)](https://crates.io/crates/kameo) 9 | [![Crates.io License](https://img.shields.io/crates/l/kameo)](https://crates.io/crates/kameo) 10 | [![GitHub Contributors](https://img.shields.io/github/contributors-anon/tqwewe/kameo)](https://github.com/tqwewe/kameo/graphs/contributors) 11 | 12 | [![Kameo banner image](https://github.com/tqwewe/kameo/blob/main/docs/banner.png?raw=true)](https://github.com/tqwewe/kameo) 13 | 14 | ## Introduction 15 | 16 | **Kameo** is a high-performance, lightweight Rust library for building fault-tolerant, asynchronous actor-based systems. Designed to scale from small, local applications to large, distributed systems, Kameo simplifies concurrent programming by providing a robust actor model that seamlessly integrates with Rust's async ecosystem. 17 | 18 | Whether you're building a microservice, a real-time application, or an embedded system, Kameo offers the tools you need to manage concurrency, recover from failures, and scale efficiently. 19 | 20 | ## Key Features 21 | 22 | - **Lightweight Actors**: Create actors that run in their own asynchronous tasks, leveraging Tokio for efficient concurrency. 23 | - **Fault Tolerance**: Build resilient systems with supervision strategies that automatically recover from actor failures. 24 | - **Flexible Messaging**: Supports both bounded and unbounded message channels, with backpressure management for load control. 25 | - **Local and Distributed Communication**: Seamlessly send messages between actors, whether they're on the same node or across the network. 26 | - **Panic Safety**: Actors are isolated; a panic in one actor doesn't bring down the whole system. 27 | - **Type-Safe Interfaces**: Strong typing for messages and replies ensures compile-time correctness. 28 | - **Easy Integration**: Compatible with existing Rust async code, and can be integrated into larger systems effortlessly. 29 | 30 | ## Why Kameo? 31 | 32 | Kameo is designed to make concurrent programming in Rust approachable and efficient. By abstracting the complexities of async and concurrent execution, Kameo lets you focus on writing the business logic of your actors without worrying about the underlying mechanics. 33 | 34 | Kameo is not just for distributed applications; it's equally powerful for local concurrent systems. Its flexible design allows you to start with a simple, single-node application and scale up to a distributed architecture when needed. 35 | 36 | ## Use Cases 37 | 38 | - **Concurrent Applications**: Simplify the development of applications that require concurrency, such as web servers, data processors, or simulation engines. 39 | - **Distributed Systems**: Build scalable microservices, distributed databases, or message brokers that require robust communication across nodes. 40 | - **Real-Time Systems**: Ideal for applications where low-latency communication is critical, such as gaming servers, chat applications, or monitoring dashboards. 41 | - **Embedded and IoT Devices**: Deploy lightweight actors on resource-constrained devices for efficient and reliable operation. 42 | - **Fault-Tolerant Services**: Create services that need to remain operational even when parts of the system fail. 43 | 44 | ## Getting Started 45 | 46 | ### Prerequisites 47 | 48 | - Rust installed (use [rustup](https://rustup.rs) for installation) 49 | - Familiarity with asynchronous programming in Rust (recommended but not required) 50 | 51 | ### Installation 52 | 53 | Add Kameo as a dependency in your `Cargo.toml` file: 54 | 55 | ```toml 56 | [dependencies] 57 | kameo = "0.16" 58 | ``` 59 | 60 | Alternatively, you can add it via command line: 61 | 62 | ```bash 63 | cargo add kameo 64 | ``` 65 | 66 | ## Basic Example 67 | 68 | ### Defining an Actor 69 | 70 | ```rust,ignore 71 | use kameo::Actor; 72 | use kameo::message::{Context, Message}; 73 | 74 | // Implement the actor 75 | #[derive(Actor)] 76 | struct Counter { 77 | count: i64, 78 | } 79 | 80 | // Define message 81 | struct Inc { amount: i64 } 82 | 83 | // Implement message handler 84 | impl Message for Counter { 85 | type Reply = i64; 86 | 87 | async fn handle(&mut self, msg: Inc, _ctx: &mut Context) -> Self::Reply { 88 | self.count += msg.amount; 89 | self.count 90 | } 91 | } 92 | ``` 93 | 94 | ### Spawning and Interacting with the Actor 95 | 96 | ```rust,ignore 97 | // Spawn the actor and obtain its reference 98 | let actor_ref = Counter::spawn(Counter { count: 0 }); 99 | 100 | // Send messages to the actor 101 | let count = actor_ref.ask(Inc { amount: 42 }).await?; 102 | assert_eq!(count, 42); 103 | ``` 104 | 105 | ## Distributed Actor Communication 106 | 107 | Kameo provides built-in support for distributed actors, allowing you to send messages across network boundaries as if they were local. 108 | 109 | ### Registering an Actor 110 | 111 | ```rust,ignore 112 | // Spawn and register the actor 113 | let actor_ref = MyActor::spawn(MyActor::default()); 114 | actor_ref.register("my_actor").await?; 115 | ``` 116 | 117 | ### Looking Up and Messaging a Remote Actor 118 | 119 | ```rust,ignore 120 | // Lookup the remote actor 121 | if let Some(remote_actor_ref) = RemoteActorRef::::lookup("my_actor").await? { 122 | let count = remote_actor_ref.ask(&Inc { amount: 10 }).await?; 123 | println!("Incremented! Count is {count}"); 124 | } 125 | ``` 126 | 127 | ### Under the Hood 128 | 129 | Kameo uses [libp2p](https://libp2p.io) for peer-to-peer networking, enabling actors to communicate over various protocols (TCP/IP, WebSockets, QUIC, etc.) using multiaddresses. This abstraction allows you to focus on your application's logic without worrying about the complexities of network programming. 130 | 131 | ## Documentation and Resources 132 | 133 | - **[API Documentation](https://docs.rs/kameo)**: Detailed information on Kameo's API. 134 | - **[The Kameo Book](https://docs.page/tqwewe/kameo)**: Comprehensive guide with tutorials and advanced topics. 135 | - **[Crate on Crates.io](https://crates.io/crates/kameo)**: Latest releases and version information. 136 | - **[Community Discord](https://discord.gg/GMX4DV9fbk)**: Join the discussion, ask questions, and share your experiences. 137 | - **[Comparing Rust Actor Libraries](https://theari.dev/blog/comparing-rust-actor-libraries/)**: Read a blog post comparing Actix, Coerce, Kameo, Ractor, and Xtra. 138 | 139 | ## Examples 140 | 141 | Explore more examples in the [examples](https://github.com/tqwewe/kameo/tree/main/examples) directory of the repository. 142 | 143 | - **Basic Actor**: How to define and interact with a simple actor. 144 | - **Distributed Actors**: Setting up actors that communicate over the network. 145 | - **Actor Pools**: Using an actor pool with the `ActorPool` actor. 146 | - **PubSub Actors**: Using a pubsub pattern with the `PubSub` actor. 147 | - **Attaching Streams**: Attaching streams to an actor. 148 | 149 | ## Contributing 150 | 151 | We welcome contributions from the community! Here are ways you can contribute: 152 | 153 | - **Reporting Issues**: Found a bug or have a feature request? [Open an issue](https://github.com/tqwewe/kameo/issues). 154 | - **Improving Documentation**: Help make our documentation better by submitting pull requests. 155 | - **Contributing Code**: Check out the [CONTRIBUTING.md](https://github.com/tqwewe/kameo/blob/main/CONTRIBUTING.md) for guidelines. 156 | - **Join the Discussion**: Participate in discussions on [Discord](https://discord.gg/GMX4DV9fbk). 157 | 158 | ## Support 159 | 160 | [![Sponsor](https://img.shields.io/badge/sponsor-ffffff?logo=githubsponsors)](https://github.com/sponsors/tqwewe) 161 | 162 | If you find Kameo useful and would like to support its development, please consider [sponsoring me on GitHub](https://github.com/sponsors/tqwewe). Your support helps me maintain and improve the project! 163 | 164 | ### Special Thanks to Our Sponsors 165 | 166 | A huge thank you to [**Huly Labs**], [**Caido Community**], and [**vanhouc**] for supporting Kameo's development! 💖 167 | 168 | [![Huly Labs](https://avatars.githubusercontent.com/u/87086734?s=100&v=4)](https://huly.io/) 169 |     170 | [![Caido Community](https://avatars.githubusercontent.com/u/168573261?s=100&v=4)](https://github.com/caido-community) 171 |     172 | [![vanhouc](https://avatars.githubusercontent.com/u/3475140?s=100&v=4)](https://github.com/vanhouc) 173 | 174 | [**Huly Labs**]: https://huly.io/ 175 | [**Caido Community**]: https://github.com/caido-community 176 | [**vanhouc**]: https://github.com/vanhouc 177 | 178 | ## License 179 | 180 | `kameo` is dual-licensed under either: 181 | 182 | - MIT License ([LICENSE-MIT](LICENSE-MIT) or ) 183 | - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or ) 184 | 185 | You may choose either license to use this software. 186 | 187 | --- 188 | 189 | [Introduction](#introduction) | [Key Features](#key-features) | [Why Kameo?](#why-kameo) | [Use Cases](#use-cases) | [Getting Started](#getting-started) | [Basic Example](#basic-example) | [Distributed Actor Communication](#distributed-actor-communication) | [Examples](#examples) | [Documentation](#documentation-and-resources) | [Contributing](#contributing) | [Support](#support) | [License](#license) 190 | 191 | -------------------------------------------------------------------------------- /actors/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kameo_actors" 3 | description = "Utility actors for kameo" 4 | version = "0.1.0" 5 | edition = "2024" 6 | readme = "../README.md" 7 | repository = "https://github.com/tqwewe/kameo" 8 | license = "MIT OR Apache-2.0" 9 | categories = ["asynchronous", "concurrency", "rust-patterns"] 10 | keywords = ["actor", "tokio", "broker", "pool", "pubsub"] 11 | 12 | [dependencies] 13 | futures.workspace = true 14 | glob = "0.3.2" 15 | kameo.workspace = true 16 | tokio.workspace = true 17 | 18 | [dev-dependencies] 19 | tokio-test = "0.4.4" 20 | -------------------------------------------------------------------------------- /actors/src/broker.rs: -------------------------------------------------------------------------------- 1 | //! Provides a topic-based message broker for the actor system. 2 | //! 3 | //! The `broker` module implements a flexible topic-based publish/subscribe mechanism that allows 4 | //! actors to communicate based on hierarchical topics rather than direct references. It supports 5 | //! glob pattern matching for topic subscriptions, allowing for powerful and flexible message routing. 6 | //! 7 | //! # Features 8 | //! 9 | //! - **Topic-Based Routing**: Messages are routed based on their topic rather than direct actor references. 10 | //! - **Pattern Matching**: Subscriptions use glob patterns, supporting wildcards and hierarchical topics. 11 | //! - **Multiple Delivery Strategies**: Configure how messages are delivered to handle different reliability needs. 12 | //! - **Automatic Cleanup**: Dead actor references are automatically removed from subscription lists. 13 | //! 14 | //! # Example 15 | //! 16 | //! ``` 17 | //! use kameo::Actor; 18 | //! use kameo_actors::broker::{Broker, Subscribe, Publish}; 19 | //! use kameo_actors::DeliveryStrategy; 20 | //! use glob::Pattern; 21 | //! # use std::time::Duration; 22 | //! # use kameo::message::{Context, Message}; 23 | //! 24 | //! #[derive(Actor, Clone)] 25 | //! struct TemperatureUpdate(f32); 26 | //! 27 | //! #[derive(Actor)] 28 | //! struct TemperatureSensor; 29 | //! 30 | //! #[derive(Actor)] 31 | //! struct DisplayActor; 32 | //! 33 | //! # impl Message for DisplayActor { 34 | //! # type Reply = (); 35 | //! # async fn handle(&mut self, msg: TemperatureUpdate, ctx: &mut Context) -> Self::Reply { } 36 | //! # } 37 | //! 38 | //! # tokio_test::block_on(async { 39 | //! // Create a broker with best effort delivery 40 | //! let broker = Broker::::new(DeliveryStrategy::BestEffort); 41 | //! let broker_ref = MyActor::spawn(broker); 42 | //! 43 | //! // Create a display actor and subscribe to kitchen temperature updates 44 | //! let display = MyActor::spawn(DisplayActor); 45 | //! broker_ref.tell(Subscribe { 46 | //! topic: Pattern::new("sensors/kitchen/*").unwrap(), 47 | //! recipient: display.recipient(), 48 | //! }).await?; 49 | //! 50 | //! // Publish a temperature update 51 | //! broker_ref.tell(Publish { 52 | //! topic: "sensors/kitchen/temperature".to_string(), 53 | //! message: TemperatureUpdate(22.5), 54 | //! }).await?; 55 | //! # Ok::<(), Box>(()) 56 | //! # }); 57 | //! ``` 58 | 59 | use std::collections::HashMap; 60 | 61 | use glob::{MatchOptions, Pattern}; 62 | use kameo::prelude::*; 63 | 64 | use crate::DeliveryStrategy; 65 | 66 | /// A generic topic-based message broker for the actor system. 67 | /// 68 | /// The broker manages subscriptions to topics and delivers messages published 69 | /// to those topics according to the specified delivery strategy. 70 | /// 71 | /// Topics use glob pattern matching syntax, allowing for flexible subscription patterns: 72 | /// - `sensors/*` - Any topic starting with "sensors/" 73 | /// - `*/temperature` - Any topic ending with "/temperature" 74 | /// - `sensors/*/humidity` - Match any topic with "sensors/" prefix and "/humidity" suffix 75 | #[derive(Actor, Clone, Debug, Default)] 76 | pub struct Broker { 77 | subscriptions: HashMap>>, 78 | delivery_strategy: DeliveryStrategy, 79 | } 80 | 81 | impl Broker { 82 | /// Creates a new broker with the specified delivery strategy. 83 | /// 84 | /// # Arguments 85 | /// 86 | /// * `delivery_strategy` - Determines how messages are delivered to subscribers 87 | /// 88 | /// # Returns 89 | /// 90 | /// A new `Broker` instance with the specified delivery strategy 91 | pub fn new(delivery_strategy: DeliveryStrategy) -> Self { 92 | Broker { 93 | subscriptions: HashMap::new(), 94 | delivery_strategy, 95 | } 96 | } 97 | 98 | fn unsubscribe(&mut self, pattern: &Pattern, actor_id: ActorID) { 99 | if let Some(recipients) = self.subscriptions.get_mut(pattern) { 100 | recipients.retain(|recipient| recipient.id() != actor_id); 101 | if recipients.is_empty() { 102 | self.subscriptions.remove(pattern); 103 | } 104 | } 105 | } 106 | } 107 | 108 | /// Message for subscribing an actor to a topic pattern. 109 | /// 110 | /// When an actor subscribes to a topic pattern, it will receive all messages 111 | /// published to topics that match that pattern. 112 | #[derive(Clone, Debug)] 113 | pub struct Subscribe { 114 | /// The pattern to subscribe to, using glob syntax 115 | pub topic: Pattern, 116 | /// The recipient that will receive messages published to matching topics 117 | pub recipient: Recipient, 118 | } 119 | 120 | impl Message> for Broker { 121 | type Reply = (); 122 | 123 | async fn handle( 124 | &mut self, 125 | Subscribe { topic, recipient }: Subscribe, 126 | _ctx: &mut Context, 127 | ) -> Self::Reply { 128 | self.subscriptions.entry(topic).or_default().push(recipient); 129 | } 130 | } 131 | 132 | /// Message for unsubscribing an actor from topics. 133 | /// 134 | /// Can unsubscribe from a specific topic pattern or all patterns. 135 | #[derive(Clone, Debug, PartialEq, Eq)] 136 | pub struct Unsubscribe { 137 | /// The specific topic pattern to unsubscribe from. 138 | /// If None, unsubscribe from all topic patterns. 139 | pub topic: Option, 140 | /// The ID of the actor to unsubscribe. 141 | pub actor_id: ActorID, 142 | } 143 | 144 | impl Message for Broker { 145 | type Reply = (); 146 | 147 | async fn handle( 148 | &mut self, 149 | Unsubscribe { topic, actor_id }: Unsubscribe, 150 | _ctx: &mut Context, 151 | ) -> Self::Reply { 152 | match topic { 153 | Some(topic) => { 154 | self.unsubscribe(&topic, actor_id); 155 | } 156 | None => { 157 | self.subscriptions.retain(|_, recipients| { 158 | recipients.retain(|recipient| recipient.id() != actor_id); 159 | !recipients.is_empty() 160 | }); 161 | } 162 | } 163 | } 164 | } 165 | 166 | /// Message for publishing content to a specific topic. 167 | /// 168 | /// When a message is published to a topic, it will be delivered to all actors 169 | /// that have subscribed to matching topic patterns, according to the broker's 170 | /// delivery strategy. 171 | #[derive(Clone, Debug, PartialEq, Eq)] 172 | pub struct Publish { 173 | /// The exact topic to publish to (not a pattern) 174 | pub topic: String, 175 | /// The message payload to deliver to subscribers 176 | pub message: M, 177 | } 178 | 179 | impl Message> for Broker { 180 | type Reply = (); 181 | 182 | async fn handle( 183 | &mut self, 184 | Publish { topic, message }: Publish, 185 | ctx: &mut Context, 186 | ) -> Self::Reply { 187 | let options = MatchOptions { 188 | case_sensitive: true, 189 | require_literal_separator: true, 190 | require_literal_leading_dot: false, 191 | }; 192 | 193 | let mut to_remove = Vec::new(); 194 | for (pattern, recipients) in &self.subscriptions { 195 | if pattern.matches_with(&topic, options) { 196 | for recipient in recipients { 197 | match self.delivery_strategy { 198 | DeliveryStrategy::Guaranteed => { 199 | let res = recipient.tell(message.clone()).await; 200 | if let Err(SendError::ActorNotRunning(_)) = res { 201 | to_remove.push((pattern.clone(), recipient.id())); 202 | } 203 | } 204 | DeliveryStrategy::BestEffort => { 205 | let res = recipient.tell(message.clone()).try_send(); 206 | if let Err(SendError::ActorNotRunning(_)) = res { 207 | to_remove.push((pattern.clone(), recipient.id())); 208 | } 209 | } 210 | DeliveryStrategy::TimedDelivery(duration) => { 211 | let res = recipient 212 | .tell(message.clone()) 213 | .mailbox_timeout(duration) 214 | .await; 215 | if let Err(SendError::ActorNotRunning(_)) = res { 216 | to_remove.push((pattern.clone(), recipient.id())); 217 | } 218 | } 219 | DeliveryStrategy::Spawned => { 220 | let pattern = pattern.clone(); 221 | let recipient = recipient.clone(); 222 | let message = message.clone(); 223 | let broker_ref = ctx.actor_ref(); 224 | tokio::spawn(async move { 225 | let res = recipient.tell(message).send().await; 226 | if let Err(SendError::ActorNotRunning(_)) = res { 227 | let _ = broker_ref 228 | .tell(Unsubscribe { 229 | topic: Some(pattern), 230 | actor_id: recipient.id(), 231 | }) 232 | .await; 233 | } 234 | }); 235 | } 236 | DeliveryStrategy::SpawnedWithTimeout(duration) => { 237 | let pattern = pattern.clone(); 238 | let recipient = recipient.clone(); 239 | let message = message.clone(); 240 | let broker_ref = ctx.actor_ref(); 241 | tokio::spawn(async move { 242 | let res = recipient 243 | .tell(message) 244 | .mailbox_timeout(duration) 245 | .send() 246 | .await; 247 | if let Err(SendError::ActorNotRunning(_)) = res { 248 | let _ = broker_ref 249 | .tell(Unsubscribe { 250 | topic: Some(pattern), 251 | actor_id: recipient.id(), 252 | }) 253 | .await; 254 | } 255 | }); 256 | } 257 | } 258 | } 259 | } 260 | } 261 | 262 | for (pattern, actor_id) in to_remove { 263 | self.unsubscribe(&pattern, actor_id); 264 | } 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /actors/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! General-purpose actors for concurrent programming 2 | //! 3 | //! This crate provides reusable actor components: 4 | //! 5 | //! - `broker`: Topic-based message broker 6 | //! - `message_bus`: Type-based message bus 7 | //! - `pool`: Actor pool for managing concurrent task execution 8 | //! - `pubsub`: Publish-subscribe pattern implementation for actor communication 9 | //! 10 | //! # When to use MessageBus vs Broker vs PubSub 11 | //! 12 | //! - Use **MessageBus** when you want to route messages based on their type without explicit topics. 13 | //! - Use **Broker** when you need hierarchical topics, pattern-based subscriptions, or explicit routing. 14 | //! - Use **PubSub** when you need simple broadcast to all listeners with optional predicate-based filtering. 15 | 16 | use std::time::Duration; 17 | 18 | pub mod broker; 19 | pub mod message_bus; 20 | pub mod pool; 21 | pub mod pubsub; 22 | 23 | /// Strategies for delivering messages to subscribers. 24 | /// 25 | /// Different strategies provide different trade-offs between reliability, 26 | /// performance, and resource usage. 27 | #[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)] 28 | pub enum DeliveryStrategy { 29 | /// Block until all messages are delivered. 30 | /// 31 | /// This strategy ensures reliable delivery but may cause the broker 32 | /// to block if any recipient's mailbox is full. 33 | Guaranteed, 34 | 35 | /// Skip actors with full mailboxes. 36 | /// 37 | /// This strategy attempts to deliver messages immediately without blocking, 38 | /// but will skip recipients whose mailboxes are full. 39 | #[default] 40 | BestEffort, 41 | 42 | /// Try to deliver with timeout (blocks the publisher). 43 | /// 44 | /// This strategy waits for each recipient to accept the message, but only 45 | /// up to the specified timeout duration. The broker will block during delivery. 46 | TimedDelivery(Duration), 47 | 48 | /// Spawn a task for each delivery (non-blocking). 49 | /// 50 | /// This strategy spawns a separate task for each message delivery, 51 | /// allowing the broker to continue processing other messages immediately. 52 | /// Tasks will retry indefinitely if mailboxes are full. 53 | Spawned, 54 | 55 | /// Spawn a task with timeout for each delivery. 56 | /// 57 | /// This strategy combines the benefits of spawned delivery with a timeout, 58 | /// ensuring that delivery attempts don't consume resources indefinitely. 59 | SpawnedWithTimeout(Duration), 60 | } 61 | -------------------------------------------------------------------------------- /actors/src/message_bus.rs: -------------------------------------------------------------------------------- 1 | //! Provides a type-based message bus for the actor system. 2 | //! 3 | //! The `message_bus` module implements a type-based publish/subscribe mechanism that allows 4 | //! actors to communicate based on message types rather than direct references or topics. 5 | //! Messages are automatically routed to actors that have registered to receive that specific type. 6 | //! 7 | //! # Features 8 | //! 9 | //! - **Type-Based Routing**: Messages are automatically routed based on their type. 10 | //! - **Automatic Type Inference**: No need to specify message types explicitly when publishing. 11 | //! - **Multiple Delivery Strategies**: Configure how messages are delivered to handle different reliability needs. 12 | //! - **Automatic Cleanup**: Dead actor references are automatically removed from subscription lists. 13 | //! 14 | //! # Example 15 | //! 16 | //! ``` 17 | //! use kameo::Actor; 18 | //! use kameo_actors::message_bus::{MessageBus, Register, Publish}; 19 | //! use kameo_actors::DeliveryStrategy; 20 | //! # use kameo::message::{Context, Message}; 21 | //! 22 | //! #[derive(Clone)] 23 | //! struct TemperatureUpdate(f32); 24 | //! 25 | //! #[derive(Actor)] 26 | //! struct TemperatureSensor; 27 | //! 28 | //! #[derive(Actor)] 29 | //! struct DisplayActor; 30 | //! 31 | //! # impl Message for DisplayActor { 32 | //! # type Reply = (); 33 | //! # async fn handle(&mut self, msg: TemperatureUpdate, ctx: &mut Context) -> Self::Reply { } 34 | //! # } 35 | //! 36 | //! # tokio_test::block_on(async { 37 | //! // Create a message bus with best effort delivery 38 | //! let message_bus = MessageBus::new(DeliveryStrategy::BestEffort); 39 | //! let message_bus_ref = MessageBuf::spawn(message_bus); 40 | //! 41 | //! // Create a display actor and register it for temperature updates 42 | //! let display = MessageBuf::spawn(DisplayActor); 43 | //! message_bus_ref.tell(Register(display.recipient::())).await?; 44 | //! 45 | //! // Publish a temperature update - automatically routes to all registered handlers 46 | //! message_bus_ref.tell(Publish(TemperatureUpdate(22.5))).await?; 47 | //! # Ok::<(), Box>(()) 48 | //! # }); 49 | //! ``` 50 | 51 | use std::{ 52 | any::{Any, TypeId}, 53 | collections::HashMap, 54 | marker::PhantomData, 55 | }; 56 | 57 | use kameo::prelude::*; 58 | 59 | use crate::DeliveryStrategy; 60 | 61 | /// A type-based message bus for broadcasting messages to registered actors. 62 | /// 63 | /// The `MessageBus` routes messages to actors based on the message type. Actors register 64 | /// to receive specific message types, and the bus automatically delivers messages to all 65 | /// registered recipients when a message of that type is published. 66 | /// 67 | /// Messages are delivered according to the configured delivery strategy, allowing 68 | /// for different reliability and performance trade-offs. 69 | #[derive(Actor, Debug, Default)] 70 | pub struct MessageBus { 71 | subscriptions: HashMap>, 72 | delivery_strategy: DeliveryStrategy, 73 | } 74 | 75 | impl MessageBus { 76 | /// Creates a new message bus with the specified delivery strategy. 77 | /// 78 | /// # Arguments 79 | /// 80 | /// * `delivery_strategy` - Determines how messages are delivered to subscribers 81 | /// 82 | /// # Returns 83 | /// 84 | /// A new `MessageBus` instance with the specified delivery strategy 85 | pub fn new(delivery_strategy: DeliveryStrategy) -> Self { 86 | MessageBus { 87 | subscriptions: HashMap::new(), 88 | delivery_strategy, 89 | } 90 | } 91 | 92 | fn unsubscribe(&mut self, actor_id: &ActorID) { 93 | let type_id = TypeId::of::(); 94 | if let Some(recipients) = self.subscriptions.get_mut(&type_id) { 95 | recipients.retain(|reg| ®.actor_id != actor_id); 96 | if recipients.is_empty() { 97 | self.subscriptions.remove(&type_id); 98 | } 99 | } 100 | } 101 | } 102 | 103 | /// Message for registering an actor to receive messages of a specific type. 104 | /// 105 | /// When an actor is registered with the message bus using this message, it will 106 | /// receive all future messages of the specified type that are published to the bus. 107 | #[derive(Clone, Debug)] 108 | pub struct Register(pub Recipient); 109 | 110 | impl Message> for MessageBus { 111 | type Reply = (); 112 | 113 | async fn handle( 114 | &mut self, 115 | Register(recipient): Register, 116 | _ctx: &mut Context, 117 | ) -> Self::Reply { 118 | self.subscriptions 119 | .entry(TypeId::of::()) 120 | .or_default() 121 | .push(Registration::new(recipient)); 122 | } 123 | } 124 | 125 | /// Message for unregistering an actor from receiving messages of a specific type. 126 | /// 127 | /// When an actor is unregistered, it will no longer receive messages of the 128 | /// specified type from the message bus. 129 | #[derive(Clone, Debug, PartialEq, Eq)] 130 | pub struct Unregister { 131 | actor_id: ActorID, 132 | phantom: PhantomData, 133 | } 134 | 135 | impl Unregister { 136 | /// Creates a new `Unregister` message for the specified actor. 137 | /// 138 | /// # Arguments 139 | /// 140 | /// * `actor_id` - The ID of the actor to unregister 141 | /// 142 | /// # Returns 143 | /// 144 | /// A new `Unregister` message that can be sent to the message bus 145 | pub fn new(actor_id: ActorID) -> Self { 146 | Unregister { 147 | actor_id, 148 | phantom: PhantomData, 149 | } 150 | } 151 | } 152 | 153 | impl Message> for MessageBus { 154 | type Reply = (); 155 | 156 | async fn handle( 157 | &mut self, 158 | Unregister { actor_id, .. }: Unregister, 159 | _ctx: &mut Context, 160 | ) -> Self::Reply { 161 | self.unsubscribe::(&actor_id); 162 | } 163 | } 164 | 165 | /// Message for publishing a value to all registered actors. 166 | /// 167 | /// When a message is published using this wrapper, it will be delivered to all 168 | /// actors that have registered to receive messages of this type. 169 | #[derive(Clone, Debug, PartialEq, Eq)] 170 | pub struct Publish(pub M); 171 | 172 | impl Message> for MessageBus { 173 | type Reply = (); 174 | 175 | async fn handle( 176 | &mut self, 177 | Publish(message): Publish, 178 | ctx: &mut Context, 179 | ) -> Self::Reply { 180 | let mut to_remove = Vec::new(); 181 | 182 | if let Some(registrations) = self.subscriptions.get(&TypeId::of::()) { 183 | for Registration { 184 | actor_id, 185 | recipient, 186 | } in registrations 187 | { 188 | let recipient: &Recipient = recipient.downcast_ref().unwrap(); 189 | match self.delivery_strategy { 190 | DeliveryStrategy::Guaranteed => { 191 | let res = recipient.tell(message.clone()).await; 192 | if let Err(SendError::ActorNotRunning(_)) = res { 193 | to_remove.push(*actor_id); 194 | } 195 | } 196 | DeliveryStrategy::BestEffort => { 197 | let res = recipient.tell(message.clone()).try_send(); 198 | if let Err(SendError::ActorNotRunning(_)) = res { 199 | to_remove.push(*actor_id); 200 | } 201 | } 202 | DeliveryStrategy::TimedDelivery(duration) => { 203 | let res = recipient 204 | .tell(message.clone()) 205 | .mailbox_timeout(duration) 206 | .await; 207 | if let Err(SendError::ActorNotRunning(_)) = res { 208 | to_remove.push(*actor_id); 209 | } 210 | } 211 | DeliveryStrategy::Spawned => { 212 | let actor_id = *actor_id; 213 | let recipient = recipient.clone(); 214 | let message = message.clone(); 215 | let message_bus_ref = ctx.actor_ref(); 216 | tokio::spawn(async move { 217 | let res = recipient.tell(message).send().await; 218 | if let Err(SendError::ActorNotRunning(_)) = res { 219 | let _ = message_bus_ref.tell(Unregister::::new(actor_id)).await; 220 | } 221 | }); 222 | } 223 | DeliveryStrategy::SpawnedWithTimeout(duration) => { 224 | let actor_id = *actor_id; 225 | let recipient = recipient.clone(); 226 | let message = message.clone(); 227 | let message_bus_ref = ctx.actor_ref(); 228 | tokio::spawn(async move { 229 | let res = recipient 230 | .tell(message) 231 | .mailbox_timeout(duration) 232 | .send() 233 | .await; 234 | if let Err(SendError::ActorNotRunning(_)) = res { 235 | let _ = message_bus_ref.tell(Unregister::::new(actor_id)).await; 236 | } 237 | }); 238 | } 239 | } 240 | } 241 | } 242 | 243 | for actor_id in to_remove { 244 | self.unsubscribe::(&actor_id); 245 | } 246 | } 247 | } 248 | 249 | #[derive(Debug)] 250 | struct Registration { 251 | actor_id: ActorID, 252 | recipient: Box, 253 | } 254 | 255 | impl Registration { 256 | fn new(recipient: Recipient) -> Self { 257 | Registration { 258 | actor_id: recipient.id(), 259 | recipient: Box::new(recipient), 260 | } 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tqwewe/kameo/0918b883712e6a5c0dd46df371c0b89eb9c710c8/banner.png -------------------------------------------------------------------------------- /benches/overhead.rs: -------------------------------------------------------------------------------- 1 | use criterion::{criterion_group, criterion_main, Criterion}; 2 | use kameo::prelude::*; 3 | use tokio::runtime::Builder; 4 | use tokio::sync::mpsc; 5 | use tokio::sync::oneshot; 6 | 7 | #[derive(Actor)] 8 | struct MyActor; 9 | 10 | impl Message for MyActor { 11 | type Reply = u32; 12 | 13 | async fn handle(&mut self, msg: u32, _ctx: &mut Context) -> Self::Reply { 14 | msg 15 | } 16 | } 17 | 18 | fn actor_benchmarks(c: &mut Criterion) { 19 | let mut group = c.benchmark_group("Kameo Actor"); 20 | 21 | // Bounded actor benchmarks 22 | group.bench_function("bounded_ask", |b| { 23 | let rt = Builder::new_current_thread().build().unwrap(); 24 | let _guard = rt.enter(); 25 | let actor_ref = rt.block_on(async { 26 | let actor_ref = MyActor::spawn_with_mailbox(MyActor, mailbox::bounded(10)); 27 | actor_ref.ask(0).send().await.unwrap(); // Ask an initial message to make sure the actor is ready 28 | actor_ref 29 | }); 30 | b.to_async(&rt).iter(|| async { 31 | actor_ref.ask(0).send().await.unwrap(); 32 | }); 33 | }); 34 | 35 | // Unbounded actor benchmarks 36 | group.bench_function("unbounded_ask", |b| { 37 | let rt = Builder::new_current_thread().build().unwrap(); 38 | let _guard = rt.enter(); 39 | let actor_ref = rt.block_on(async { 40 | let actor_ref = MyActor::spawn_with_mailbox(MyActor, mailbox::unbounded()); 41 | actor_ref.ask(0).send().await.unwrap(); // Ask an initial message to make sure the actor is ready 42 | actor_ref 43 | }); 44 | b.to_async(&rt).iter(|| async { 45 | actor_ref.ask(0).send().await.unwrap(); 46 | }); 47 | }); 48 | 49 | group.finish(); 50 | } 51 | 52 | fn plain_benchmarks(c: &mut Criterion) { 53 | let mut group = c.benchmark_group("Plain Tokio Task"); 54 | 55 | // Bounded channel benchmarks 56 | group.bench_function("bounded_ask", |b| { 57 | let rt = Builder::new_current_thread().build().unwrap(); 58 | let (tx, mut rx) = mpsc::channel::<(u32, oneshot::Sender)>(10); 59 | rt.spawn(async move { 60 | while let Some((msg, tx)) = rx.recv().await { 61 | tx.send(msg).unwrap(); 62 | } 63 | }); 64 | 65 | b.to_async(&rt).iter(|| async { 66 | let (reply_tx, reply_rx) = oneshot::channel(); 67 | tx.send((0, reply_tx)).await.unwrap(); 68 | reply_rx.await.unwrap(); 69 | }); 70 | }); 71 | 72 | // Unbounded channel benchmarks 73 | group.bench_function("unbounded_ask", |b| { 74 | let rt = Builder::new_current_thread().build().unwrap(); 75 | let (tx, mut rx) = mpsc::unbounded_channel::<(u32, oneshot::Sender)>(); 76 | rt.spawn(async move { 77 | while let Some((msg, tx)) = rx.recv().await { 78 | tx.send(msg).unwrap(); 79 | } 80 | }); 81 | 82 | b.to_async(&rt).iter(|| async { 83 | let (reply_tx, reply_rx) = oneshot::channel(); 84 | tx.send((0, reply_tx)).unwrap(); 85 | reply_rx.await.unwrap(); 86 | }); 87 | }); 88 | 89 | group.finish(); 90 | } 91 | 92 | criterion_group!(benches, actor_benchmarks, plain_benchmarks); 93 | criterion_main!(benches); 94 | -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | # git-cliff ~ configuration file 2 | # https://git-cliff.org/docs/configuration 3 | 4 | [bump] 5 | features_always_bump_minor = false 6 | breaking_always_bump_major = false 7 | 8 | [changelog] 9 | # changelog header 10 | header = """ 11 | # Changelog\n 12 | All notable changes to this project will be documented in this file. 13 | 14 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 15 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n 16 | """ 17 | # template for the changelog body 18 | # https://keats.github.io/tera/docs/#introduction 19 | body = """ 20 | {% if version -%} 21 | ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} 22 | {% else -%} 23 | ## [Unreleased] 24 | {% endif -%} 25 | {%- for contributor in github.contributors | filter(attribute="is_first_time", value=true) %} 26 | * @{{ contributor.username }} made their first contribution in #{{ contributor.pr_number }} 27 | {% endfor -%} 28 | {% for group, commits in commits | group_by(attribute="group") %} 29 | ### {{ group | upper_first }} 30 | {% for commit in commits | sort(attribute="author.timestamp") | reverse | sort(attribute="breaking") | reverse %} 31 | - {% if commit.breaking %}**BREAKING:** {% endif %}{{ commit.message | upper_first }}\ 32 | {% if commit.remote.pr_number %}{% else %} [](https://github.com/tqwewe/kameo/commit/{{ commit.id }}){%- endif %}\ 33 | {% endfor %} 34 | {% endfor %}\n 35 | """ 36 | # template for the changelog footer 37 | footer = """ 38 | {% for release in releases -%} 39 | {% if release.version -%} 40 | {% if release.previous.version -%} 41 | [{{ release.version | trim_start_matches(pat="v") }}]: \ 42 | https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}\ 43 | /compare/{{ release.previous.version }}..{{ release.version }} 44 | {% endif -%} 45 | {% else -%} 46 | [unreleased]: https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}\ 47 | /compare/{{ release.previous.version }}..HEAD 48 | {% endif -%} 49 | {% endfor %} 50 | 51 | """ 52 | # remove the leading and trailing whitespace from the templates 53 | trim = true 54 | 55 | [git] 56 | # parse the commits based on https://www.conventionalcommits.org 57 | conventional_commits = true 58 | # filter out the commits that are not conventional 59 | filter_unconventional = true 60 | # process each line of a commit as an individual commit 61 | split_commits = false 62 | commit_parsers = [ 63 | { message = "^chore: bump version to .*", skip = true }, 64 | { message = "^chore: bump to version .*", skip = true }, 65 | { message = "^.*\\(example(s)?\\):.*", skip = true }, 66 | { message = "^chore(\\(.*\\))?: .*", group = "Misc" }, 67 | { message = "^feat(\\(.*\\))?: .*", group = "Added" }, 68 | { message = "^docs(\\(.*\\))?: .*", group = "Documentation" }, 69 | { message = "^.*: add", group = "Added" }, 70 | { message = "^.*: support", group = "Added" }, 71 | { message = "^.*: remove", group = "Removed" }, 72 | { message = "^.*: delete", group = "Removed" }, 73 | { message = "^test", group = "Fixed" }, 74 | { message = "^fix", group = "Fixed" }, 75 | { message = "^.*: fix", group = "Fixed" }, 76 | { message = "^.*", group = "Changed" }, 77 | ] # regex for parsing and grouping commits 78 | # protect breaking changes from being skipped due to matching a skipping commit_parser 79 | protect_breaking_commits = false 80 | # filter out the commits that are not matched by commit parsers 81 | filter_commits = true 82 | # regex for matching git tags 83 | tag_pattern = "v[0-9].*" 84 | # regex for skipping tags 85 | skip_tags = "beta|alpha" 86 | # regex for ignoring tags 87 | ignore_tags = "" 88 | # sort the tags topologically 89 | topo_order = false 90 | # sort the commits inside sections by oldest/newest order 91 | sort_commits = "oldest" 92 | 93 | [remote.remote] 94 | owner = "tqwewe" 95 | repo = "kameo" 96 | -------------------------------------------------------------------------------- /docs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Kameo", 3 | "description": "Distributed, Fault-tolerant Async Actors Powered by Tokio", 4 | "socialPreview": "https://repository-images.githubusercontent.com/779318723/b1ce92be-760d-427b-b3a6-ffca8cbc2e6a", 5 | "logo": { 6 | "light": "https://github.com/tqwewe/kameo/raw/refs/heads/main/docs/farron.png", 7 | "dark": "https://github.com/tqwewe/kameo/raw/refs/heads/main/docs/farron.png" 8 | }, 9 | "scripts": { 10 | "googleAnalytics": "G-FES40CNKHX" 11 | }, 12 | "sidebar": [ 13 | { 14 | "group": "Getting Started", 15 | "pages": [ 16 | { 17 | "title": "Introduction", 18 | "href": "/", 19 | "icon": "globe" 20 | }, 21 | { 22 | "title": "Getting Started", 23 | "href": "/getting-started", 24 | "icon": "rocket" 25 | } 26 | ] 27 | }, 28 | { 29 | "group": "Core Concepts", 30 | "pages": [ 31 | { 32 | "title": "Overview", 33 | "href": "/core-concepts", 34 | "icon": "book" 35 | }, 36 | { 37 | "title": "Actors", 38 | "href": "/core-concepts/actors", 39 | "icon": "users" 40 | }, 41 | { 42 | "title": "Messages", 43 | "href": "/core-concepts/messages", 44 | "icon": "envelope" 45 | }, 46 | { 47 | "title": "Requests", 48 | "href": "/core-concepts/requests", 49 | "icon": "question" 50 | }, 51 | { 52 | "title": "Replies", 53 | "href": "/core-concepts/replies", 54 | "icon": "reply" 55 | }, 56 | { 57 | "title": "Supervision", 58 | "href": "/core-concepts/supervision", 59 | "icon": "shield-alt" 60 | } 61 | ] 62 | }, 63 | { 64 | "group": "Distributed Actors", 65 | "pages": [ 66 | { 67 | "title": "Overview", 68 | "href": "/distributed-actors", 69 | "icon": "network-wired" 70 | }, 71 | { 72 | "title": "Bootstrapping the Actor Swarm", 73 | "href": "/distributed-actors/bootstrapping-actor-swarm", 74 | "icon": "play-circle" 75 | }, 76 | { 77 | "title": "Dialing and Connecting to Other Nodes", 78 | "href": "/distributed-actors/dialing-connecting-nodes", 79 | "icon": "link" 80 | }, 81 | { 82 | "title": "Registering and Looking up Actors", 83 | "href": "/distributed-actors/registering-looking-up-actors", 84 | "icon": "user-tag" 85 | }, 86 | { 87 | "title": "Messaging Remote Actors", 88 | "href": "/distributed-actors/messaging-remote-actors", 89 | "icon": "envelope-open-text" 90 | } 91 | ] 92 | }, 93 | { 94 | "group": "Additional Resources", 95 | "pages": [ 96 | { 97 | "title": "FAQ", 98 | "href": "/faq", 99 | "icon": "question-circle" 100 | } 101 | ] 102 | } 103 | ] 104 | } -------------------------------------------------------------------------------- /docs/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tqwewe/kameo/0918b883712e6a5c0dd46df371c0b89eb9c710c8/docs/banner.png -------------------------------------------------------------------------------- /docs/core-concepts.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Core Concepts Overview 3 | --- 4 | 5 | Welcome to the Core Concepts section of Kameo, where you'll find the foundational principles and components that make up the Kameo actor library. Understanding these concepts is crucial for effectively leveraging Kameo to build resilient, scalable, and concurrent applications. Below is an overview of the key concepts that form the backbone of Kameo's design and functionality. 6 | 7 | ## [  Actors](/core-concepts/actors) 8 | 9 | At the heart of Kameo lies the Actor model, a powerful abstraction that encapsulates state and behavior into independent, concurrent units. Actors are the primary building blocks of applications in Kameo, designed to isolate state and handle messages asynchronously. This isolation enhances fault tolerance and system resilience, as actors can fail and recover without affecting the overall system stability. 10 | 11 | ## [  Messages](/core-concepts/messages) 12 | 13 | Communication in Kameo is achieved through messages. Actors interact with each other exclusively by sending and receiving messages, ensuring a loose coupling between components. This messaging system underpins the asynchronous, non-blocking nature of Kameo, enabling efficient communication patterns and facilitating the development of responsive applications. 14 | 15 | ## [  Requests](/core-concepts/requests) 16 | 17 | Requests represent a specialized form of message exchange in Kameo, supporting both the "ask" and "tell" patterns. This library allows actors to either send messages without expecting a reply ("tell") or send messages and await responses ("ask"). The ask pattern, in particular, introduces a way to handle more complex interaction flows, including synchronous operations and error handling. 18 | 19 | ## [  Replies](/core-concepts/replies) 20 | 21 | Replies are responses to requests, completing the communication cycle between actors. By implementing the `Reply` trait, apps can define custom reply types, ensuring that actors can exchange meaningful data and status information. This mechanism is crucial for implementing robust and interactive systems where actors depend on the outcomes of their interactions. 22 | 23 | ## [  Supervision](/core-concepts/supervision) 24 | 25 | Supervision is a strategy for managing actor lifecycle and failure recovery, embodying the principle of "let it crash." In Kameo, actors can be linked together in supervision trees, allowing parent actors to monitor and respond to the failures of their children. This model provides a structured approach to error handling and recovery, ensuring system resilience and stability. 26 | -------------------------------------------------------------------------------- /docs/core-concepts/actors.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Actors Overview 3 | --- 4 | 5 | The core of Kameo is its actors. Actors are objects that encapsulate state and behavior. They interact with the rest of the system asynchronously, primarily through message passing, which allows for a high degree of concurrency and scalability. This section provides an overview of the `Actor` trait in Kameo, detailing its lifecycle, messaging, and supervision. 6 | 7 | ## Actor Trait Overview 8 | 9 | The `Actor` trait defines the essential functionality and lifecycle hooks for an actor in Kameo. Implementing this trait allows you to create custom actors tailored to your application's requirements. Here are the key components of the `Actor` trait: 10 | 11 | - **Lifecycle Hooks**: Kameo provides several hooks (`on_start`, `on_stop`, `on_panic`, `on_link_died`) that are called at different points in an actor's lifecycle. These hooks offer points of intervention where custom behavior can be implemented, such as initialization, cleanup, and error handling. 12 | - **Mailbox**: Each actor has a mailbox (`type Mailbox`), which can be bounded or unbounded. The mailbox is where incoming messages are queued before being processed by the actor. Bounded mailboxes help in applying backpressure, preventing the system from being overwhelmed by too many messages. 13 | - **Messaging**: Actors communicate by sending messages to each other. When an actor is spawned, it returns an `ActorRef`, a reference to the actor that can be used to send messages to it. Messages are sent asynchronously and are processed sequentially by the receiving actor. 14 | - **Supervision**: Actors can supervise other actors, allowing for hierarchical error handling and recovery strategies. The `on_panic` and `on_link_died` hooks are integral to this, enabling actors to respond to failures in their child actors, as well as themselves. 15 | 16 | ### **Deriving Actor** 17 | 18 | To streamline the creation of actors and reduce repetitive boilerplate, Kameo offers a derive macro for the `Actor` trait. This macro not only simplifies the actor definition process but also provides sensible defaults that adhere to common practices. 19 | 20 | When using the derive macro, you can customize your actor with the following attributes: 21 | 22 | - `#[actor(name = "...")]`: This attribute allows you to assign a custom name to your actor. By default, Kameo uses the actor's identifier (ident) as its name. Specifying a custom name can be useful for logging. 23 | - `#[actor(mailbox = ...)]`: Through this attribute, you can define the type of mailbox your actor should use. Kameo supports two mailbox types: `bounded` and `unbounded`. 24 | - **Bounded Mailbox**: For a `bounded` mailbox, you have the option to specify its capacity using the syntax `bounded()`, where `` represents the maximum number of messages the mailbox can hold. If not specified, a default size of 1,000 is used. 25 | - **Unbounded Mailbox**: An `unbounded` mailbox does not have a size limit, meaning it can grow indefinitely as more messages are received. While this ensures that no message is ever rejected due to mailbox capacity, it could potentially lead to increased memory usage under high load or if the actor is unable to process messages quickly enough. 26 | 27 | **Example** 28 | 29 | ```rust 30 | use kameo::Actor; 31 | 32 | #[derive(Actor)] 33 | #[actor(name = "MyAmazingActor", mailbox = bounded(64))] 34 | struct MyActor { } 35 | ``` 36 | 37 | ## Lifecycle Management 38 | 39 | - **Starting**: The `on_start` hook is called before the actor starts processing messages. It's an opportunity to perform any necessary initialization. 40 | - **Stopping**: Actors are stopped either explicitly or when all references to their `ActorRef` are dropped. The `on_stop` hook allows for cleanup activities before the actor is fully stopped. 41 | - **Error Handling**: The `on_panic` hook is invoked when an actor panics or encounters an error while processing a message. This hook can decide whether the actor should be stopped or continue processing messages. 42 | - **Link Failures**: The `on_link_died` hook is called when a linked actor dies, providing a chance to react to the failure of closely related actors. 43 | 44 | ## Actor Creation and Messaging 45 | 46 | Creating an actor involves implementing the `Actor` trait and then spawning the actor using `Actor::spawn`. Upon spawning, an `ActorRef` is returned, which is used to send messages to the actor. The actor processes messages using the `handle` method from the `Message` trait, optionally returning a reply. 47 | 48 | ### Example Usage 49 | 50 | ```rust 51 | struct MyActor; 52 | 53 | impl Actor for MyActor { 54 | type Mailbox = BoundedMailbox; 55 | 56 | async fn on_start(&mut self, actor_ref: ActorRef) -> Result<(), BoxError> { 57 | println!("Actor started"); 58 | Ok(()) 59 | } 60 | 61 | // Implement other lifecycle hooks as needed... 62 | } 63 | 64 | // Spawning the actor 65 | let actor_ref = MyActor::spawn(MyActor); 66 | ``` 67 | 68 | The above example demonstrates defining a simple actor and spawning it. The actor prints a message upon starting, showcasing the use of the `on_start` lifecycle hook. 69 | 70 | --- 71 | 72 | #### Summary 73 | 74 | Actors form the core of Kameo, providing a structured way to encapsulate both logic and state within self-contained units. This design principle facilitates the creation of systems that are inherently more scalable and resilient, enabling you to build applications that efficiently manage concurrency and are robust against failures. Through actors, complex interactions become manageable, allowing for a modular approach to system architecture. 75 | -------------------------------------------------------------------------------- /docs/core-concepts/messages.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Messages Overview 3 | --- 4 | 5 | In Kameo, messages play a central role in the communication between actors. They are the primary means through which actors interact and modify each other's state. Understanding how messages work is fundamental to effectively using Kameo to build concurrent applications. 6 | 7 | ## **Defining Messages** 8 | 9 | Messages in Kameo are any static types that are handled by implementing the `Message` trait for an actor. This design allows for a wide variety of message types, from simple data structures to complex commands with associated data. The flexibility in message definition enables developers to design their actor system with clear and concise communication patterns. 10 | 11 | Here's a closer look at the `Message` trait: 12 | 13 | ```rust 14 | pub trait Message: Actor { 15 | /// The reply sent back to the message caller. 16 | type Reply: Reply; 17 | 18 | /// Handler for this message. 19 | async fn handle( 20 | &mut self, 21 | msg: T, 22 | ctx: &mut Context, 23 | ) -> Self::Reply; 24 | } 25 | ``` 26 | 27 | - **Trait Generics**: The `Message` trait is generic over `T`, which represents the type of the message being sent to the actor. This allows for the implementation of message handlers that are type-safe and specific to the message being handled. 28 | - **Reply Type**: Each message has an associated `Reply` type, which is the type of the response that the message sender can expect to receive. This reply must implement the `Reply` trait, which ensures that it can be properly handled and sent back to the caller. 29 | - **Message Handler**: The core of the Message trait is the handle function. This function is where the logic for handling a specific message is defined. It takes mutable access to the actor (&mut self), the message itself (msg), and a context (ctx) that provides access to actor-specific functionality like sending messages to other actors or accessing the actor's state. The handle function returns a future that resolves to the message's reply type, allowing for asynchronous processing of messages. This design supports non-blocking message handling, which is essential for building responsive and scalable actor systems. 30 | 31 | ## Sequential Processing 32 | 33 | Messages in Kameo are processed sequentially, one at a time, with exclusive mutable access to the actor's state. This sequential processing model simplifies state management within actors, as there is no need for explicit synchronization mechanisms like locks. When an actor is handling a message, it can safely modify its state without worrying about concurrent modifications from other messages. 34 | 35 | This model also ensures that messages are processed in the order they are received, which can be critical for maintaining consistency and correctness in certain applications. 36 | 37 | ## Asynchronous and Concurrent 38 | 39 | While messages are processed sequentially within a single actor, Kameo allows for concurrent processing across multiple actors. This is where the actor model shines, enabling high levels of concurrency without the complexity associated with traditional multithreading and synchronization. 40 | 41 | The asynchronous nature of the `handle` function, combined with Rust's powerful futures and async/await syntax, makes it straightforward to perform non-blocking operations, such as I/O tasks or querying other actors, within a message handler. 42 | 43 | --- 44 | 45 | #### Summary 46 | 47 | Messages are the primary means of communication between actors in Kameo, serving as the backbone for asynchronous information exchange. This approach promotes a high degree of decoupling, allowing components within a system to interact flexibly and evolve over time. By effectively utilizing messages, you can design interactions that are both clear and adaptable, driving the functionality and responsiveness of your application. 48 | -------------------------------------------------------------------------------- /docs/core-concepts/replies.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Replies Overview 3 | --- 4 | 5 | In Kameo, replies are the responses sent back from an actor following the receipt and processing of a message. These replies are fundamental to the ask pattern of actor communication, where a sender awaits a response to proceed. Ensuring that replies are properly structured and handled is crucial for maintaining the flow of information and control within your actor system. 6 | 7 | ## The Reply Trait 8 | 9 | To facilitate communication within Kameo, all types intended to serve as responses from an actor must implement the `Reply` trait. This designation ensures that a type is recognized as a valid form of reply, capable of being processed within the actor's messaging system. 10 | 11 | Special attention is given to replies that encapsulate a `Result::Err` variant from the Rust standard library. These are treated distinctively compared to their non-error counterparts. Specifically, when a message is dispatched using the "tell" method, any errors emerging from the actor's message handler are interpreted as panics. This mechanism highlights the critical role of thorough error handling and validation in an actor's message processing routine, safeguarding against unexpected terminations. 12 | 13 | While most standard library types already implement the `Reply` trait, there might be exceptions. Should you encounter a standard library type not implementing `Reply`, you are encouraged to report this through an issue. For types outside the standard library that do not implement `Reply`, a straightforward workaround is to wrap your reply type in a `Result`, where `T` is your original type. Given that any `Result` type inherently implements `Reply`, this approach often obviates the need for custom implementations of the `Reply` trait for your types, especially in scenarios where using `Result` types is a common practice. 14 | 15 | ## Deriving the Reply Trait 16 | 17 | Kameo simplifies the implementation of the `Reply` trait through the `#[derive(Reply)]` macro. This macro automatically provides the necessary trait implementations for a type, making it straightforward to create custom reply types that integrate seamlessly with Kameo's messaging system. 18 | 19 | ### Example Usage 20 | 21 | ```rust 22 | use kameo::Reply; 23 | 24 | #[derive(Reply)] 25 | pub struct MyReply { 26 | pub data: String, 27 | pub status: bool, 28 | } 29 | 30 | // Usage within an actor 31 | impl Message for MyActor { 32 | type Reply = MyReply; 33 | 34 | async fn handle( 35 | &mut self, 36 | msg: MyRequest, 37 | _: &mut Context, 38 | ) -> Self::Reply { 39 | // Logic to process the message and generate a reply 40 | MyReply { 41 | data: "Processed data".to_string(), 42 | status: true, 43 | } 44 | } 45 | } 46 | ``` 47 | 48 | In this example, `MyReply` is defined as a struct with data relevant to the response expected by a sender. By deriving the `Reply` trait, `MyReply` is automatically equipped to serve as a response in Kameo's messaging system. This pattern allows for rich, structured data to be communicated back to senders, facilitating complex interactions and workflows within your actor system. 49 | 50 | ## Handling Replies 51 | 52 | When dealing with ask requests, it's important to handle replies gracefully. This involves not only receiving the reply but also managing potential timeouts and errors that might occur during the interaction. If the message handler returned an error while processing a message, it will be returned as a `SendError::HandlerError`. Kameo's design encourages clear, concise handling of these scenarios, ensuring that your actor system remains robust and resilient under various operational conditions. 53 | 54 | --- 55 | 56 | #### Summary 57 | 58 | Replies play a crucial role in the communication loop between actors in Kameo, particularly when direct feedback or data needs to be conveyed back to the requester. They enrich the interaction model, enabling more structured and predictable exchanges. By defining clear and meaningful replies, you ensure smoother and more reliable communication flows, which is essential for handling complex and evolving workflows within your applications. 59 | -------------------------------------------------------------------------------- /docs/core-concepts/requests.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Requests Overview 3 | --- 4 | 5 | In the context of the Kameo, requests are the means through which actors communicate and interact with each other. These interactions are encapsulated in two primary forms: ask requests and tell requests. Understanding the nuances between these two types of requests is crucial for effectively managing actor behaviors and ensuring the robustness of your application. 6 | 7 | ## Ask Requests 8 | 9 | Ask requests are a form of message sending where the sender waits for a response from the receiver. This pattern is useful when the sender requires data or confirmation that an action has been completed before proceeding. Unlike tell requests, ask requests inherently support handling responses and errors, providing a direct way to deal with exceptional conditions. 10 | 11 | Key Features of Ask Requests: 12 | 13 | - **Reply Awaited**: The sender pauses its execution and waits for a reply, making it synchronous in nature within an asynchronous execution model. 14 | - **Error Handling**: If an actor encounters an error while processing an ask request, the responsibility of handling these errors falls to the caller. This allows for more granular error management strategies. 15 | - **Timeouts**: 16 | - **Mailbox Timeout**: For actors with a bounded mailbox, an optional `mailbox_timeout` can be specified. This timeout represents the maximum duration the request will wait in the queue before being processed. If the mailbox is full beyond this duration, the request may be dropped or an error returned. 17 | - **Reply Timeout**: A `reply_timeout` can also be set, indicating how long the sender will wait for a response. This is particularly useful for avoiding indefinite blocking in scenarios where the receiver might be unable to process the request promptly. 18 | 19 | ## Tell Requests 20 | 21 | Tell requests, on the other hand, are the "fire-and-forget" type of messages. When a tell request is sent, the sender does not wait for any acknowledgment or reply from the receiver. This approach is ideal for notifications or commands where the outcome does not directly influence the sender's immediate actions. 22 | 23 | Characteristics of Tell Requests: 24 | 25 | - **No Reply**: The sender continues its execution without waiting for a response, embodying a truly asynchronous interaction pattern. 26 | - **Error Handling**: Errors encountered by the actor while processing a tell request are treated as panics. By default, such panics may lead to the stopping of the actor, although this behavior can be customized via the `Actor::on_panic` hook to allow for error recovery or logging. 27 | - **Mailbox Timeout**: Similar to ask requests, a `mailbox_timeout` can be set for tell requests sent to actors with bounded mailboxes. This timeout helps manage the queuing behavior in scenarios where the actor's mailbox might be at capacity, ensuring that the system can gracefully handle backpressure. 28 | 29 | --- 30 | 31 | #### Summary 32 | 33 | Requests, encompassing both the ask and tell patterns, offer versatile communication strategies within Kameo. This duality provides the flexibility to either await responses for critical operations or to proceed without direct feedback for more autonomous actions. Such versatility is key to supporting a broad spectrum of application requirements, from straightforward notifications to intricate data exchanges and control flows, enhancing the dynamism and efficiency of your actor interactions. 34 | -------------------------------------------------------------------------------- /docs/core-concepts/supervision.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Supervision Overview 3 | --- 4 | 5 | Supervision is a critical concept in building resilient actor systems, ensuring that the system can recover from failures and continue operating without interruption. In Kameo, supervision and actor lifecycle management are facilitated through a combination of customizable behavior hooks and actor linking. This page discusses how to effectively use these features to create robust actor hierarchies and manage actor failures gracefully. 6 | 7 | ## Customizing Actor Behavior on Panic 8 | 9 | When an actor panics, its default behavior can be customized using the `on_panic` hook within the `Actor` trait. This hook allows developers to define custom logic that should execute when an actor encounters a panic, providing a first line of defense in managing unexpected failures. 10 | 11 | ## Linking Actors 12 | 13 | Beyond individual actor behavior, Kameo supports linking actors together to create a supervision tree. This structure enables actors to monitor each other's health and respond to failures, forming the backbone of a self-healing system. 14 | 15 | ### Actor Links 16 | 17 | Actors can be linked using `ActorRef::link`, which establishs a sibling relationship between the two actors. 18 | 19 | Remote actors can be linked using the `ActorRef::link_remote` and `RemoteActorRef::link_remote` methods. 20 | 21 | ### Handling Link Failures 22 | 23 | When a linked actor dies, the surviving actors can react to this event using the `on_link_died` hook in the `Actor` trait. 24 | This hook provides the ID of the deceased actor and the reason for its termination, enabling the surviving actors to implement custom logic, such as restarting the failed actor or taking other remedial actions. 25 | 26 | The default behavior for `on_link_died` is to stop the current actor if the linked actor died for any reason other than a normal shutdown. This conservative default ensures that failures are not silently ignored, promoting system stability by preventing dependent actors from continuing in an inconsistent state. 27 | 28 | In the case of remote actor links, if a peer/node gets disconnected, then all links to actors on that peer will be considered dead, with `ActorStopReason::PeerDisconnected` being signaled to the linked actors. 29 | 30 | ## Unlinking Actors 31 | 32 | In some scenarios, it may be necessary to remove links between actors, either to restructure the supervision tree or in response to changing application dynamics. Kameo provides the following methods for this purpose: 33 | 34 | - `ActorRef::unlink`: Unlinks two previously linked sibling actors. 35 | - `ActorRef::unlink_remote`: Unlinks two previously linked sibling remote actors. 36 | - `RemoteActorRef::unlink_remote`: Unlinks two previously linked sibling remote actors. 37 | 38 | These methods allows for dynamic adjustments to the actor supervision hierarchy, ensuring that the system can adapt to new requirements or recover from errors by reorganizing actor relationships. 39 | 40 | --- 41 | 42 | #### Summary 43 | 44 | Supervision in Kameo is a powerful mechanism for building resilient, self-healing actor systems. By leveraging customizable panic behavior and actor linking, developers can design systems that are capable of recovering from failures, maintaining consistent operation through disruptions. Whether through parent-child hierarchies or peer relationships, actor links in Kameo provide the foundation for a robust supervision strategy, ensuring that actor systems can gracefully handle and recover from the inevitable challenges they face. 45 | -------------------------------------------------------------------------------- /docs/distributed-actors.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Distributed Actors 3 | --- 4 | 5 | Kameo’s distributed actor system enables actors to communicate seamlessly across different nodes in a decentralized network. This architecture provides a powerful foundation for building fault-tolerant, scalable, and resilient distributed systems. At its core, Kameo leverages [libp2p](https://libp2p.io) for peer-to-peer communication and a decentralized actor registry. 6 | 7 | ## Key Components 8 | 9 | - **ActorSwarm**: The central structure responsible for managing peer-to-peer communication between nodes. It initializes the libp2p swarm, manages connections, and coordinates actor registration and message routing. 10 | - **RemoteActorRef**: A reference to an actor that resides on a remote node. This reference abstracts away the networking details and allows you to interact with remote actors using familiar APIs. 11 | - **Multiaddress**: libp2p uses multiaddresses to specify how nodes can be reached using different network protocols like TCP, WebSockets, or QUIC. Multiaddresses are essential for routing messages between nodes. 12 | - **Kademlia DHT**: Kameo uses Kademlia DHT to store and retrieve actor registration details. When an actor is registered, its name is stored as a key in the DHT, and the value contains information about how to reach the actor, making it discoverable by other nodes. 13 | 14 | ## How Distributed Actors Work 15 | 16 | 1. **Bootstrapping the Actor Swarm**: Each node in the network must initialize an `ActorSwarm` to participate in the distributed system. The swarm manages the libp2p connections and allows the node to register actors and send/receive messages. 17 | 18 | 2. **Actor Registration**: After bootstrapping the swarm, actors can be registered under unique names. Once registered, the actor’s address is propagated through the Kademlia DHT, making it discoverable by other nodes. 19 | 20 | 3. **Remote Messaging**: Once an actor is registered, other nodes can look it up and send messages to it. The `RemoteActorRef` allows you to send and receive messages across the network without worrying about the underlying networking. 21 | 22 | ## Why Use Distributed Actors? 23 | 24 | Distributed actors are ideal for building systems that need to scale horizontally across multiple machines or geographic locations. They enable fault-tolerant systems, as nodes can fail or be added without disrupting the overall network. Some common use cases for distributed actors include: 25 | 26 | - **Real-Time Systems**: Chat systems, multiplayer games, and real-time data monitoring where actors need to communicate with low latency. 27 | - **Microservices Architecture**: Building independent services that communicate via message passing, while maintaining resilience to failures. 28 | - **IoT Systems**: Distributed control systems where devices communicate with each other using lightweight actors. 29 | 30 | ## Key Benefits 31 | 32 | - **Decentralized Architecture**: No single point of failure. Actors can be registered, discovered, and messaged from any node in the network. 33 | - **Fault Tolerance**: The system continues to operate even if nodes fail or become unreachable. Actor registration via Kademlia DHT ensures nodes can find each other. 34 | - **Scalability**: Add more nodes to the swarm to handle more actors and distribute load across the network. 35 | - **Flexible Networking**: By using libp2p, Kameo supports various network protocols, allowing it to adapt to different environments and infrastructure. 36 | 37 | --- 38 | 39 | #### What’s Next? 40 | 41 | Now that you have an understanding of how distributed actors work, you’re ready to dive deeper into the specifics of setting up the actor swarm, registering actors, and messaging across nodes. 42 | 43 | Explore the next section on [Bootstrapping the Actor Swarm](/distributed-actors/bootstrapping-actor-swarm) to get started with setting up your distributed actor system. 44 | -------------------------------------------------------------------------------- /docs/distributed-actors/bootstrapping-actor-swarm.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Bootstrapping Actor Swarm 3 | --- 4 | 5 | Before actors can communicate across nodes in a distributed system, you need to initialize the `ActorSwarm`. The `ActorSwarm` is responsible for managing peer-to-peer connections, actor registration, and message routing across nodes. This section explains how to bootstrap the swarm, set up network listening, and prepare your system for distributed communication. 6 | 7 | ## Initializing the Actor Swarm 8 | 9 | The first step in setting up distributed actors is bootstrapping the `ActorSwarm`. This initializes the swarm, allowing the node to participate in the network and accept incoming connections. 10 | 11 | ```rust 12 | let actor_swarm = ActorSwarm::bootstrap()?; 13 | ``` 14 | 15 | This will initialize the swarm with default settings, preparing the node to listen for connections from other nodes. Once bootstrapped, you can register actors and send messages. 16 | 17 | ## Bootstrapping with Identity 18 | 19 | In some cases, you may want to bootstrap the swarm with a specific identity (keypair). This allows nodes to be uniquely identified in the network, which can be useful for secure communication or when interacting with known peers. 20 | 21 | ```rust 22 | let keypair = Keypair::generate(); 23 | let actor_swarm = ActorSwarm::bootstrap_with_identity(keypair)?; 24 | ``` 25 | 26 | The `Keypair` generates a cryptographic identity for the node. This is useful for secure, verifiable communication between peers. The node will now be identified by its `PeerId`, derived from the keypair. 27 | 28 | ## Bootstrapping with Custom Behaviour 29 | 30 | For more advanced use cases such as custom swarm configurations, or entirely custom behaviour, the actor swarm can be bootstrapped with the more flexible methods: 31 | 32 | - `ActorSwarm::bootstrap_with_behaviour` for providing a custom behaviour. 33 | - `ActorSwarm::bootstrap_with_swarm` for providing a custom swarm. 34 | - `ActorSwarm::bootstrap_manual` for extremely manual processing of the actor swarm. (see [`examples/manual_swarm.rs`]) 35 | 36 | [`examples/manual_swarm.rs`]: https://github.com/tqwewe/kameo/blob/main/examples/manual_swarm.rs 37 | 38 | ## Listening on a Multiaddress 39 | 40 | After bootstrapping the swarm, you need to instruct the node to listen on a specific network address. Kameo uses libp2p’s **multiaddress** format, which allows nodes to specify how they can be reached over the network. Multiaddresses define the protocol (e.g., TCP or QUIC) and the IP address/port combination. 41 | 42 | ```rust 43 | actor_swarm.listen_on("/ip4/0.0.0.0/udp/8020/quic-v1".parse()?).await?; 44 | ``` 45 | 46 | In this example, the node is listening on the IP address `0.0.0.0` (which represents all available network interfaces) on UDP port `8020`, using the QUIC protocol. You can customize this address based on your network environment or use different protocols as needed. 47 | 48 | ## Example: Bootstrapping and Listening 49 | 50 | Here’s a full example that combines bootstrapping the swarm and setting up a listener: 51 | 52 | ```rust 53 | #[tokio::main] 54 | async fn main() -> Result<(), Box> { 55 | // Bootstrap the swarm with a default identity 56 | let actor_swarm = ActorSwarm::bootstrap()?; 57 | 58 | // Start listening on a multiaddress 59 | actor_swarm.listen_on("/ip4/0.0.0.0/udp/8020/quic-v1".parse()?).await?; 60 | 61 | // The node is now ready to register actors and communicate with other nodes 62 | Ok(()) 63 | } 64 | ``` 65 | 66 | Once the swarm is bootstrapped and listening, the node can register actors, dial other peers, and send messages. This setup ensures the node is part of the network and ready to accept and route messages. 67 | 68 | --- 69 | 70 | #### What’s Next? 71 | 72 | Now that your node is part of the distributed system, it’s time to explore how to connect to other nodes and establish communication. In the next section, we’ll cover how to dial and connect to peers using multiaddresses. 73 | 74 | Explore the next section on [Dialing and Connecting to Other Nodes](/distributed-actors/dialing-connecting-nodes) for more details. 75 | -------------------------------------------------------------------------------- /docs/distributed-actors/dialing-connecting-nodes.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Dialing and Connecting to Other Nodes 3 | --- 4 | 5 | Once your node is bootstrapped and listening on an address, the next step in setting up a distributed actor system is dialing and connecting to other nodes. Kameo’s `ActorSwarm` allows nodes to discover and connect with peers using libp2p’s peer-to-peer communication capabilities. This section covers how to dial peers using multiaddresses, how to manage peer connections, and how libp2p’s mDNS feature can simplify peer discovery. 6 | 7 | ## Dialing a Peer 8 | 9 | To establish a connection with another node, you need to dial its multiaddress. A multiaddress specifies the protocol, IP address, and port required to reach the node. 10 | 11 | ```rust 12 | actor_swarm.dial("/ip4/192.0.2.0/udp/8020/quic-v1".parse()?).await?; 13 | ``` 14 | 15 | In this example, the node is dialing another node located at IP address `192.0.2.0` on UDP port `8020`, using the QUIC protocol. Once the connection is established, the node can interact with remote actors on the peer. 16 | 17 | ### Dialing with Options 18 | 19 | For more advanced use cases, Kameo provides the `DialOpts` structure, which allows you to customize how you dial peers. This is useful when you need to include additional details or preferences when establishing connections, such as specifying peer IDs. 20 | 21 | ```rust 22 | let dial_opts = DialOpts::unknown_peer_id() 23 | .address("/ip4/192.0.2.0/udp/8020/quic-v1".parse()?) 24 | .build(); 25 | 26 | actor_swarm.dial(dial_opts).await?; 27 | ``` 28 | 29 | In this example, `DialOpts` is used to dial a peer at a known address without specifying the peer’s ID. This option can be customized depending on the situation, such as when connecting to peers with specific identities or conditions. 30 | 31 | 32 | ## Auto-discovery with mDNS 33 | 34 | In most cases, libp2p’s [**mDNS (Multicast DNS)**](https://docs.libp2p.io/concepts/discovery-routing/mdns/) feature allows nodes to automatically discover each other on the same local network, without needing to manually dial peers. This greatly simplifies setting up a distributed system, as peers will be able to find and connect to each other without explicit configuration. 35 | 36 | If your network environment supports mDNS, it can be enabled by default in Kameo’s `ActorSwarm`. With mDNS, nodes announce themselves to the network and discover other peers in the same multicast domain. 37 | 38 | ```rust 39 | // Example of using mDNS auto-discovery 40 | let actor_swarm = ActorSwarm::bootstrap()?; 41 | actor_swarm.listen_on("/ip4/0.0.0.0/udp/8020/quic-v1".parse()?).await?; 42 | // Peers on the same local network can now discover each other automatically 43 | ``` 44 | 45 | This makes peer discovery effortless in environments where mDNS is available, such as local networks or development environments. 46 | 47 | ## Adding Peer Addresses Manually 48 | 49 | In some cases, you may want to manually add a known peer’s address to the swarm without dialing immediately. This can be useful for preloading peers or when you want to manage connections programmatically. 50 | 51 | ```rust 52 | let peer_id: PeerId = ...; // Obtained from the peer's identity 53 | let addr: Multiaddr = "/ip4/192.0.2.0/tcp/1234".parse()?; 54 | 55 | actor_swarm.add_peer_address(peer_id, addr); 56 | ``` 57 | 58 | This example shows how to associate a peer’s identity with a known address. Once the peer address is added, the swarm will use it to attempt future connections or for message routing. 59 | 60 | ## Disconnecting from Peers 61 | 62 | If you need to disconnect from a peer, you can do so using the peer’s `PeerId`. This is helpful when you want to manage peer connections and ensure that a node is no longer part of the active network. 63 | 64 | ```rust 65 | actor_swarm.disconnect_peer_id(peer_id); 66 | ``` 67 | 68 | This cleanly terminates the connection with the specified peer, removing it from the swarm’s list of active peers. 69 | 70 | ## Example: Dialing and Connecting to Peers 71 | 72 | Here’s a full example of how to dial a peer and establish a connection: 73 | 74 | ```rust 75 | #[tokio::main] 76 | async fn main() -> Result<(), Box> { 77 | // Bootstrap the swarm 78 | let actor_swarm = ActorSwarm::bootstrap()?; 79 | 80 | // Start listening on a multiaddress 81 | actor_swarm.listen_on("/ip4/0.0.0.0/udp/8020/quic-v1".parse()?).await?; 82 | 83 | // Dial a peer on a specific address 84 | actor_swarm.dial("/ip4/192.0.2.0/udp/8020/quic-v1".parse()?).await?; 85 | 86 | // The node is now connected to the peer and can send/receive messages 87 | Ok(()) 88 | } 89 | ``` 90 | 91 | This example shows how to bootstrap the swarm, set up a listener, and connect to another node using a multiaddress. 92 | 93 | --- 94 | 95 | #### What’s Next? 96 | 97 | Now that you can connect to other nodes, the next step is registering actors and looking them up across the network. This allows nodes to discover and interact with actors on remote peers. 98 | 99 | Explore the next section on [Registering and Looking up Actors](/distributed-actors/registering-looking-up-actors) to learn more about actor registration and discovery. 100 | -------------------------------------------------------------------------------- /docs/distributed-actors/messaging-remote-actors.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Messaging Remote Actors 3 | --- 4 | 5 | Once actors are registered and discoverable across nodes, the next step is to start communicating with them. Kameo allows you to send messages to remote actors just like you would with local actors. The underlying networking is handled transparently, and messages are routed across the network using the `RemoteActorRef`. This section explains how to message remote actors and handle replies. 6 | 7 | ## Sending Messages 8 | 9 | After looking up a remote actor using `RemoteActorRef`, you can send messages to it using the familiar `ask` and `tell` patterns. 10 | 11 | - **ask**: Used when you expect a reply from the remote actor. 12 | - **tell**: Used when you do not expect a reply, a "fire-and-forget" style message. 13 | 14 | ```rust 15 | // Lookup the remote actor 16 | let remote_actor_ref = RemoteActorRef::::lookup("my_actor").await?; 17 | 18 | // Send a message and await the reply 19 | if let Some(actor) = remote_actor_ref { 20 | let result = actor.ask(&Inc { amount: 10 }).await?; 21 | println!("Incremented count: {result}"); 22 | } 23 | ``` 24 | 25 | In this example, the node looks up the actor named `"my_actor"` and sends an `Inc` message to increment the actor's internal state. The message is serialized and sent over the network to the remote actor, and the reply is awaited asynchronously. 26 | 27 | ### Fire-and-Forget Messaging 28 | 29 | If you don’t need a response from the actor, you can use the `tell` method to send a message without waiting for a reply. 30 | 31 | ```rust 32 | // Send a fire-and-forget message 33 | actor.tell(&LogMessage { text: String::from("Logging event") }).await?; 34 | ``` 35 | 36 | The `tell` method is useful for one-way communication where no acknowledgment is required, such as logging or notification systems. 37 | 38 | ## Requirements for Remote Messaging 39 | 40 | There are two requirements to enable messaging between nodes: 41 | 42 | 1. **The Actor must implement `RemoteActor`**: Any actor that can be messaged remotely must implement the `RemoteActor` trait, which uniquely identifies the actor type. This allows the system to route messages to the correct actor on remote nodes. 43 | 44 | ```rust 45 | #[derive(RemoteActor)] 46 | pub struct MyActor; 47 | ``` 48 | 49 | 2. **Message Serialization with `#[remote_message]`**: In Kameo, messages sent between nodes must be serializable. To enable this, message types need to implement `Serialize` and `Deserialize` traits, and the message implementation must be annotated with the `#[remote_message]` macro, which assigns a unique identifier to the actor and message type handler. 50 | 51 | ```rust 52 | #[remote_message("3b9128f1-0593-44a0-b83a-f4188baa05bf")] 53 | impl Message for MyActor { 54 | type Reply = i64; 55 | 56 | async fn handle(&mut self, msg: Inc, _ctx: &mut Context) -> Self::Reply { 57 | self.count += msg.amount as i64; 58 | self.count 59 | } 60 | } 61 | ``` 62 | 63 | This `#[remote_message]` macro ensures that the message is properly serialized and routed to the correct actor across the network. The UUID string assigned to each message must be unique within the crate to avoid conflicts. 64 | 65 | ### Why the `#[remote_message]` Macro is Needed 66 | 67 | Unlike actor systems that use a traditional enum for message types, Kameo allows actors to handle a variety of message types without defining a centralized enum for all possible messages. This flexibility introduces a challenge when deserializing incoming messages—because we don't know the exact message type at the time of deserialization. 68 | 69 | To solve this, Kameo leverages the [**linkme**](https://crates.io/crates/linkme) crate, which dynamically builds a `HashMap` of registered message types at link time: 70 | 71 | ```rust 72 | HashMap 73 | ``` 74 | 75 | - **`RemoteMessageRegistrationID`**: A unique identifier combining the actor’s ID and the message’s ID (both provided via the `RemoteActor` and `#[remote_message]` macros). 76 | - **`RemoteMessagesFns`**: A struct containing function pointers for handling messages (`ask` or `tell`) for the given actor and message type. 77 | 78 | When a message is received, Kameo uses this hashmap to look up the appropriate function for deserializing and handling the message, based on the combination of the actor and message IDs. 79 | 80 | By using the `#[remote_message]` macro, Kameo registers each message type during link time, ensuring that when a message is received, the system knows how to deserialize it and which function to invoke on the target actor. 81 | 82 | ## Handling Replies 83 | 84 | When sending a message using the `ask` pattern, you’ll typically want to handle a response from the remote actor. The reply type is specified in the actor’s message handler and can be awaited asynchronously. 85 | 86 | ```rust 87 | let result = actor.ask(&Inc { amount: 10 }).await?; 88 | println!("Received reply: {}", result); 89 | ``` 90 | 91 | In this example, the reply from the remote actor is awaited, and the result is printed once received. 92 | 93 | ## Example: Messaging a Remote Actor 94 | 95 | Here’s a full example of how to message a remote actor and handle its reply: 96 | 97 | ```rust 98 | #[tokio::main] 99 | async fn main() -> Result<(), Box> { 100 | // Bootstrap the swarm and listen on an address 101 | let actor_swarm = ActorSwarm::bootstrap()?; 102 | actor_swarm.listen_on("/ip4/0.0.0.0/udp/8020/quic-v1".parse()?).await?; 103 | 104 | // Lookup a registered remote actor 105 | let remote_actor_ref = RemoteActorRef::::lookup("my_actor").await?; 106 | 107 | if let Some(actor) = remote_actor_ref { 108 | // Send a message and await the reply 109 | let result = actor.ask(&Inc { amount: 10 }).await?; 110 | println!("Incremented count: {result}"); 111 | } else { 112 | println!("Actor not found"); 113 | } 114 | 115 | Ok(()) 116 | } 117 | ``` 118 | 119 | In this example, a node is bootstrapped, connected to a network, and looks up a remote actor named `"my_actor"`. After finding the actor, the node sends an increment message (`Inc`) and waits for a response, which is printed upon receipt. 120 | 121 | --- 122 | 123 | #### What’s Next? 124 | 125 | Now that you’ve seen how to send messages to remote actors and handle replies, you can start building distributed systems where actors on different nodes communicate seamlessly. Experiment with sending different types of messages and handling remote interactions. 126 | 127 | If you haven’t yet set up your actor system, go back to the [Bootstrapping the Actor Swarm](/distributed-actors/bootstrapping-actor-swarm) section for instructions on setting up your distributed actor environment. 128 | -------------------------------------------------------------------------------- /docs/distributed-actors/registering-looking-up-actors.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Registering and Looking up Actors 3 | --- 4 | 5 | In a distributed system, actors need to be discoverable by other nodes so that they can receive messages from remote peers. Kameo provides actor registration and lookup mechanisms using a decentralized registry powered by **Kademlia DHT** (Distributed Hash Table). This section covers how to register actors and look them up across the network using `ActorSwarm`. 6 | 7 | ## Registering Actors 8 | 9 | After bootstrapping the `ActorSwarm` and setting up the node to listen for connections, actors can be registered under unique names. This makes them discoverable by other nodes, allowing remote actors to send messages to them. 10 | 11 | To register an actor, use the `ActorRef::register` method, which registers the actor under a specified name. 12 | 13 | ```rust 14 | // Spawn and register an actor 15 | let actor_ref = MyActor::spawn(MyActor::default()); 16 | actor_ref.register("my_actor").await?; 17 | ``` 18 | 19 | In this example, an actor of type `MyActor` is spawned and registered with the name `"my_actor"`. The name is propagated across the network using the Kademlia DHT, which stores the mapping between the actor’s name and its reference on the node. Other nodes can now look up and interact with this actor using its registered name. 20 | 21 | ## Actor Lookup 22 | 23 | Once an actor is registered, other nodes can look it up by name. The `RemoteActorRef::lookup` method allows you to retrieve a reference to an actor that is registered on a remote node. If the lookup is successful, the returned `RemoteActorRef` can be used to send messages to the remote actor, just like with local actors. 24 | 25 | ```rust 26 | // Lookup a registered remote actor 27 | let remote_actor_ref = RemoteActorRef::::lookup("my_actor").await?; 28 | 29 | if let Some(actor) = remote_actor_ref { 30 | // Use the actor reference to send a message 31 | let result = actor.ask(&Inc { amount: 10 }).await?; 32 | println!("Incremented count: {result}"); 33 | } else { 34 | println!("Actor not found"); 35 | } 36 | ``` 37 | 38 | In this example, the node attempts to look up an actor registered with the name `"my_actor"`. If the actor is found on a remote node, a `RemoteActorRef` is returned, allowing the local node to send messages to the remote actor. A `RemoteActorRef` may in fact be a reference to an actor running on the current node. 39 | 40 | ## Kademlia DHT: Decentralized Actor Lookup 41 | 42 | Kameo’s actor registration and lookup system is powered by **Kademlia DHT**, a distributed hash table used to store and retrieve actor registration details across nodes. When an actor is registered, its name is stored as a key in the DHT, and the value is a reference to the actor on the node where it was registered. 43 | 44 | This decentralized registry ensures that actors can be discovered efficiently across a network of nodes, without relying on a centralized registry or server. Each node stores a portion of the DHT and can look up actors registered on other nodes. 45 | 46 | - **Registration**: When an actor is registered, its name is propagated to other nodes using the DHT. 47 | - **Lookup**: When a node looks up an actor by name, the DHT retrieves the location of the actor from the network and returns a reference to the actor. 48 | 49 | ## Example: Registering and Looking up Actors 50 | 51 | Here’s a full example showing how to register an actor on one node and look it up from another: 52 | 53 | ```rust 54 | #[tokio::main] 55 | async fn main() -> Result<(), Box> { 56 | // Node 1: Bootstrap & register an actor 57 | let actor_swarm = ActorSwarm::bootstrap()?; 58 | // Listen so we can handle requests from other nodes 59 | actor_swarm.listen_on("/ip4/0.0.0.0/udp/8020/quic-v1".parse()?).await?; 60 | let actor_ref = MyActor::spawn(MyActor::default()); 61 | actor_ref.register("my_actor").await?; 62 | 63 | // Node 2: Bootstrap, and lookup the actor and send a message 64 | let actor_swarm = ActorSwarm::bootstrap()?; 65 | let remote_actor_ref = RemoteActorRef::::lookup("my_actor").await?; 66 | if let Some(actor) = remote_actor_ref { 67 | let result = actor.ask(&Inc { amount: 10 }).await?; 68 | println!("Incremented count: {result}"); 69 | } else { 70 | println!("Actor not found"); 71 | } 72 | 73 | Ok(()) 74 | } 75 | ``` 76 | 77 | In this example, Node 1 registers an actor with the name `"my_actor"`, and Node 2 looks up the actor using its registered name. Once the actor is found, Node 2 sends a message to it. 78 | 79 | ## Retry Mechanism for Actor Lookup 80 | 81 | In distributed systems, there can be cases where an actor is not yet registered or its registration hasn’t fully propagated through the Kademlia DHT. This can happen if the actor has just been registered or if the network is still syncing. In such cases, it’s useful to implement a retry mechanism when looking up actors, giving the system time to propagate the registration. 82 | 83 | A simple retry loop can help ensure that your node eventually finds the actor, especially in systems where actors are expected to register shortly after startup: 84 | 85 | ```rust 86 | use std::time::Duration; 87 | use tokio::time::sleep; 88 | 89 | async fn retry_lookup() -> Result>, RegistryError> { 90 | for _ in 0..5 { 91 | if let Some(actor) = RemoteActorRef::::lookup("my_actor").await? { 92 | return Ok(Some(actor)); 93 | } 94 | // Retry after a delay if actor is not found 95 | sleep(Duration::from_secs(2)).await; 96 | } 97 | println!("Actor not found after retries"); 98 | Ok(None) 99 | } 100 | ``` 101 | 102 | In this example, the lookup is retried up to 5 times, with a 2-second delay between each attempt. If the actor is registered in the meantime, the lookup will succeed and return a reference to the actor. This approach ensures more resilient lookups in scenarios where actor registration or DHT propagation may be delayed. 103 | 104 | A cleaner solution might involve using a crate such as [backon](https://crates.io/crates/backon) for retrying actor lookups. 105 | 106 | --- 107 | 108 | #### What’s Next? 109 | 110 | With actors now registered and discoverable across nodes, the next step is to explore how to send messages to remote actors. The messaging system allows you to send and receive messages between actors on different nodes using `RemoteActorRef`. 111 | 112 | Explore the next section on [Messaging Remote Actors](/distributed-actors/messaging-remote-actors) to learn more. 113 | -------------------------------------------------------------------------------- /docs/faq.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Frequently Asked Questions (FAQ) 3 | --- 4 | 5 | ## How does Kameo handle communication over a network? 6 | 7 | Kameo uses [libp2p](https://libp2p.io) for networking, with Kademlia Distributed Hash Table (DHT) under the hood for actor registration and lookup. This allows actors to communicate across nodes without needing a predefined schema, and messages are routed using multiaddresses, supporting a variety of protocols such as TCP/IP and QUIC. 8 | 9 | --- 10 | 11 | ## How do I query an actor's state? 12 | 13 | You can query an actor’s state by sending a message using the `ask` pattern, which allows you to request a response without modifying the actor's state. 14 | 15 | ```rust 16 | let result = actor_ref.ask(QueryState).await?; 17 | println!("Actor state: {:?}", result); 18 | ``` 19 | 20 | However, it’s often better to design actors in a way where they notify each other of state changes. In this push model, actors send updates when their state changes, reducing the need for constant querying and keeping interactions more efficient and decoupled. 21 | 22 | --- 23 | 24 | ## How is Kameo different from gRPC? 25 | 26 | Unlike gRPC, which requires predefined schemas and often involves significant boilerplate, Kameo allows dynamic communication with actors across nodes without the need for code generation or schema management. Actors communicate via `RemoteActorRef`, and messages are passed just like with local actors, making it more flexible and less rigid than gRPC. 27 | 28 | --- 29 | 30 | ## Why does Kameo use async for actors? 31 | 32 | Kameo's async nature allows multiple actors to run on a single thread using Tokio's runtime, which is highly efficient for handling IO-bound tasks. While many actors may be CPU-bound, async ensures that non-blocking tasks, such as network operations, can proceed without stalling other actors. 33 | 34 | --- 35 | 36 | ## Can Kameo be used for distributed systems? 37 | 38 | Yes, Kameo is built for distributed systems. Using libp2p and Kademlia DHT, actors can be registered and discovered across nodes, and they can communicate as if they were local. This makes Kameo ideal for distributed microservices or systems where actors are spread across different machines. 39 | 40 | --- 41 | 42 | ## Can Kameo be used for building parallel applications? 43 | 44 | Yes. Kameo runs on the Tokio runtime, which can be configured with the `rt-multi-thread` feature to utilize multiple cores. This allows actors to be distributed across all CPU cores, handling parallel workloads efficiently. 45 | 46 | --- 47 | 48 | ## Is Kameo production-ready? 49 | 50 | Kameo is still relatively new and under active development. It is being tested in real-world projects, but the API has seen many iterations. While Kameo is not yet widely adopted in production, it is rapidly maturing to meet production-level standards. 51 | 52 | --- 53 | 54 | ## Why are messages processed sequentially in an actor? 55 | 56 | Messages are processed sequentially within each actor to maintain consistency and correctness. This ensures that state changes happen in a well-defined order, which is crucial in applications where message processing order matters. 57 | 58 | --- 59 | 60 | ## Why does my actor stop unexpectedly? 61 | 62 | Actors stop running if one of the following conditions is met: 63 | 64 | - All references to the actor (`ActorRef`) are dropped. 65 | - It is explicitly stopped with `.stop_gracefully()` or `.kill()`. 66 | - `on_start` returns an error. 67 | - `on_panic` returns `Ok(Some(reason))`, or returns an error. 68 | 69 | If your actor is stopped, double check each of these reasons as its likely due to one of them occuring. 70 | 71 | --- 72 | 73 | ## How does Kameo compare to other Rust actor libraries like Actix or Ractor? 74 | 75 | - **Actix**: Kameo offers a simpler API with less boilerplate, especially for async use cases. Actix has seen many changes in its runtime over time, while Kameo is built directly on Tokio for more native async support. 76 | - **Ractor**: Kameo differs in several ways. Messages in Kameo are implemented as separate structs with their own `Message` trait, while Ractor uses a single enum for messages. Additionally, in Kameo, the actor itself is the state, while Ractor separates the state and actor. 77 | -------------------------------------------------------------------------------- /docs/farron.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tqwewe/kameo/0918b883712e6a5c0dd46df371c0b89eb9c710c8/docs/farron.png -------------------------------------------------------------------------------- /docs/getting-started.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started 3 | --- 4 | 5 | Welcome to Kameo, your gateway to building efficient, scalable, and resilient systems in Rust. This guide will walk you through the initial steps of installing Kameo and creating a simple "Hello, World!" application to get your feet wet in the world of asynchronous actors. 6 | 7 | ## Installation 8 | 9 | Before diving into the world of Kameo, you'll need to ensure that your Rust development environment is set up. Kameo requires Rust 1.79, which you can install or update via [rustup](https://rustup.rs/). 10 | 11 | With Rust installed, you can add Kameo to your project by editing your `Cargo.toml` file: 12 | 13 | ```toml 14 | [dependencies] 15 | kameo = "0.16" 16 | ``` 17 | 18 | Alternatively you can run `cargo add kameo`. 19 | 20 | ## Hello World Actor 21 | 22 | This example demonstrates a basic "Hello World" actor capable of handling a `Greet` message. It showcases the fundamental concepts of actor creation, message definition, and asynchronous message handling within Kameo. 23 | 24 | ### Defining the Actor and Message 25 | 26 | First, we define a `HelloWorldActor` and a `Greet` message. The actor will process messages of type `Greet`, which contain a string greeting. 27 | 28 | ```rust 29 | use kameo::prelude::*; 30 | 31 | // Define the actor 32 | #[derive(Actor)] 33 | pub struct HelloWorldActor; 34 | 35 | // Define the message 36 | pub struct Greet(String); 37 | 38 | // Implement the message handling for HelloWorldActor 39 | impl Message for HelloWorldActor { 40 | type Reply = (); // This actor sends no reply 41 | 42 | async fn handle( 43 | &mut self, 44 | Greet(greeting): Greet, // Destructure the Greet message to get the greeting string 45 | _: &mut Context, // The message handling context 46 | ) -> Self::Reply { 47 | println!("{greeting}"); // Print the greeting to the console 48 | } 49 | } 50 | ``` 51 | 52 | ### Spawning the Actor and Sending a Message 53 | 54 | To interact with the `HelloWorldActor`, we spawn it and send a `Greet` message. This is done using the `spawn` function from Kameo and the `tell` method provided by the actor's reference, `actor_ref`. 55 | 56 | ```rust 57 | use kameo::prelude::*; 58 | 59 | #[tokio::main] // Mark the entry point as an asynchronous main function 60 | async fn main() -> Result<(), Box> { // Use a Result return type for error handling 61 | // Spawn the HelloWorldActor with an unbounded mailbox 62 | let actor_ref = spawn(HelloWorldActor); 63 | 64 | // Send a Greet message to the actor 65 | actor_ref 66 | .tell(Greet("Hello, world!".to_string())) 67 | .await?; 68 | 69 | Ok(()) 70 | } 71 | ``` 72 | 73 | ### Understanding the Code 74 | 75 | - **Actor Definition**: The `HelloWorldActor` is a simple actor that does not maintain any state and only prints out the greeting it receives. 76 | - **Message Handling**: The `handle` method asynchronously processes the `Greet` message. It takes ownership of the message and a context parameter, which could be used for more advanced message handling scenarios. 77 | - **Spawning and Messaging**: The `spawn` function creates an instance of the `HelloWorldActor` and returns a reference to it (`actor_ref`). The `tell` method is then used to send a `Greet` message to the actor. The `send` method is awaited to ensure the message is sent to the actors mailbox. 78 | - **Asynchronous Runtime**: The example uses Tokio as the asynchronous runtime, indicated by the `#[tokio::main]` attribute. This is necessary for running asynchronous Rust code. 79 | 80 | --- 81 | 82 | #### Next Steps 83 | 84 | This example is a starting point for building applications with Kameo. From here, you can explore more complex actor behaviors, state management, actor supervision, and building distributed systems with remote actors. 85 | 86 | Remember, the power of actors comes from their ability to encapsulate state and behavior, process messages concurrently, and interact in a decoupled manner. Experiment with these concepts to build scalable and resilient applications. 87 | -------------------------------------------------------------------------------- /docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Welcome to Kameo 3 | image: https://github.com/tqwewe/kameo/blob/main/docs/banner.png?raw=true 4 | --- 5 | 6 | Embarking on the journey of building software, you're often faced with the challenge of managing complexity, ensuring reliability, and achieving scalability. That's where Kameo steps in—a powerful, yet approachable, actor framework built on Tokio, a proven runtime in the Rust ecosystem renowned for its amazing capabilities for asynchronous and concurrent code. Kameo is designed to help you conquer these challenges head-on, offering a simplified approach to building robust applications. 7 | 8 | ##   Simplifying Concurrency 9 | 10 | At its core, Kameo simplifies the daunting world of concurrent programming. With its actor-based model, it abstracts away the nitty-gritty details of thread management, locking, and synchronization. Instead, you get to focus on what truly matters: designing your system's logic. By encapsulating state and behavior within actors that communicate through messages, Kameo enables you to build systems that are naturally concurrent and resilient. 11 | 12 | ##   Building Resilient Systems 13 | 14 | Reliability is non-negotiable in today's software landscape. Kameo is built with this principle in mind, offering robust supervision strategies that allow your system to recover from failures gracefully. Instead of painstakingly handling every possible error scenario, you can design your system to "let it crash" and rely on Kameo to manage the recovery process, keeping your application running smoothly and reliably. 15 | 16 | ##   Scalability Made Easy 17 | 18 | Whether you're developing a small utility or a large-scale distributed system, scalability is key. Kameo's lightweight actor model and efficient message-passing mechanisms make it inherently scalable. Your application can grow from handling dozens to millions of concurrent operations with minimal changes to the underlying architecture. 19 | 20 | ##   Why Kameo? 21 | 22 | In a world where complexity is the norm, Kameo stands out by offering simplicity, resilience, and scalability. It's not just about making concurrent programming easier—it's about empowering you to build the reliable, efficient, and scalable systems that today's users demand. Whether you're a seasoned developer or just starting out, Kameo is designed to be relatable and accessible, providing you with the tools you need to bring your ideas to life. 23 | -------------------------------------------------------------------------------- /examples/basic.rs: -------------------------------------------------------------------------------- 1 | use kameo::prelude::*; 2 | 3 | #[derive(Actor, Default)] 4 | pub struct MyActor { 5 | count: i64, 6 | } 7 | 8 | // A simple increment message, returning the new count 9 | pub struct Inc { 10 | amount: u32, 11 | } 12 | 13 | impl Message for MyActor { 14 | type Reply = i64; 15 | 16 | async fn handle(&mut self, msg: Inc, _ctx: &mut Context) -> Self::Reply { 17 | println!("Incrementing count by {}", msg.amount); 18 | self.count += msg.amount as i64; 19 | self.count 20 | } 21 | } 22 | 23 | // Always returns an error 24 | pub struct ForceErr; 25 | 26 | impl Message for MyActor { 27 | type Reply = Result<(), i32>; 28 | 29 | async fn handle( 30 | &mut self, 31 | _msg: ForceErr, 32 | _ctx: &mut Context, 33 | ) -> Self::Reply { 34 | Err(3) 35 | } 36 | } 37 | 38 | #[tokio::main] 39 | async fn main() -> Result<(), Box> { 40 | let my_actor_ref = MyActor::spawn(MyActor::default()); 41 | 42 | // Increment the count by 3 43 | let count = my_actor_ref.ask(Inc { amount: 3 }).await?; 44 | println!("Count is {count}"); 45 | 46 | // Increment the count by 50 in the background 47 | my_actor_ref.tell(Inc { amount: 50 }).await?; 48 | 49 | // Increment the count by 2 50 | let count = my_actor_ref.ask(Inc { amount: 2 }).await?; 51 | println!("Count is {count}"); 52 | 53 | // Async messages that return an Err will cause the actor to panic 54 | my_actor_ref.tell(ForceErr).await?; 55 | 56 | // Actor should be stopped, so we cannot send more messages to it 57 | assert!(my_actor_ref.ask(Inc { amount: 2 }).await.is_err()); 58 | 59 | Ok(()) 60 | } 61 | -------------------------------------------------------------------------------- /examples/broker.rs: -------------------------------------------------------------------------------- 1 | use kameo::prelude::*; 2 | use kameo_actors::{ 3 | broker::{Broker, Publish, Subscribe}, 4 | DeliveryStrategy, 5 | }; 6 | 7 | #[derive(Actor)] 8 | struct MyActor; 9 | 10 | #[derive(Clone)] 11 | struct Echo { 12 | message: String, 13 | } 14 | 15 | impl Message for MyActor { 16 | type Reply = (); 17 | 18 | async fn handle(&mut self, msg: Echo, ctx: &mut Context) -> Self::Reply { 19 | println!("Actor {} says {}", ctx.actor_ref().id(), msg.message); 20 | } 21 | } 22 | 23 | #[tokio::main] 24 | async fn main() -> Result<(), Box> { 25 | let broker_ref = Broker::spawn(Broker::new(DeliveryStrategy::Guaranteed)); 26 | 27 | // Subscribe 28 | let my_actor_ref = MyActor::spawn(MyActor); 29 | broker_ref 30 | .tell(Subscribe { 31 | topic: "my-topic".parse()?, 32 | recipient: my_actor_ref.clone().recipient(), 33 | }) 34 | .await?; 35 | 36 | let my_actor_ref2 = MyActor::spawn(MyActor); 37 | broker_ref 38 | .tell(Subscribe { 39 | topic: "my-*".parse()?, 40 | recipient: my_actor_ref2.clone().recipient(), 41 | }) 42 | .await?; 43 | 44 | // Publish 45 | broker_ref 46 | .tell(Publish { 47 | topic: "my-topic".to_string(), 48 | message: Echo { 49 | message: "Hola".to_string(), 50 | }, 51 | }) 52 | .await?; 53 | 54 | // Shutdown everything 55 | broker_ref.stop_gracefully().await?; 56 | broker_ref.wait_for_shutdown().await; 57 | 58 | my_actor_ref.stop_gracefully().await?; 59 | my_actor_ref2.stop_gracefully().await?; 60 | my_actor_ref.wait_for_shutdown().await; 61 | my_actor_ref2.wait_for_shutdown().await; 62 | 63 | Ok(()) 64 | } 65 | -------------------------------------------------------------------------------- /examples/forward.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use kameo::{prelude::*, reply::ForwardedReply}; 4 | 5 | #[derive(Actor)] 6 | struct PlayersActor { 7 | player_map: HashMap>, 8 | } 9 | 10 | struct ForwardToPlayer { 11 | player_id: u64, 12 | message: M, 13 | } 14 | 15 | impl Message> for PlayersActor 16 | where 17 | Player: Message, 18 | M: Send + 'static, 19 | { 20 | type Reply = ForwardedReply>::Reply>; 21 | 22 | async fn handle( 23 | &mut self, 24 | msg: ForwardToPlayer, 25 | ctx: &mut Context, 26 | ) -> Self::Reply { 27 | let player_ref = self.player_map.get(&msg.player_id).unwrap(); 28 | ctx.forward(player_ref, msg.message).await 29 | } 30 | } 31 | 32 | #[derive(Actor, Default)] 33 | struct Player { 34 | health: f32, 35 | } 36 | 37 | struct Damage { 38 | amount: f32, 39 | } 40 | 41 | impl Message for Player { 42 | type Reply = f32; 43 | 44 | async fn handle( 45 | &mut self, 46 | Damage { amount }: Damage, 47 | _ctx: &mut Context, 48 | ) -> Self::Reply { 49 | self.health -= amount; 50 | self.health 51 | } 52 | } 53 | 54 | #[tokio::main] 55 | async fn main() -> Result<(), Box> { 56 | let player_ref = Player::spawn(Player { health: 100.0 }); 57 | 58 | let mut player_map = HashMap::new(); 59 | player_map.insert(0, player_ref.clone()); 60 | 61 | let players_ref = PlayersActor::spawn(PlayersActor { player_map }); 62 | 63 | let health = players_ref 64 | .ask(ForwardToPlayer { 65 | player_id: 0, 66 | message: Damage { amount: 38.2 }, 67 | }) 68 | .await?; 69 | println!("Player health: {health:.1}"); 70 | 71 | Ok(()) 72 | } 73 | -------------------------------------------------------------------------------- /examples/macro.rs: -------------------------------------------------------------------------------- 1 | use kameo::prelude::*; 2 | 3 | #[derive(Actor)] 4 | pub struct MyActor { 5 | count: i64, 6 | } 7 | 8 | #[messages] 9 | impl MyActor { 10 | fn new() -> Self { 11 | MyActor { count: 0 } 12 | } 13 | 14 | #[message(derive(Clone))] 15 | fn inc(&mut self, amount: u32) -> i64 { 16 | self.count += amount as i64; 17 | self.count 18 | } 19 | 20 | #[message] 21 | fn force_err(&self) -> Result<(), i32> { 22 | Err(3) 23 | } 24 | 25 | #[message] 26 | pub fn print(&self, msg: T) 27 | where 28 | T: std::fmt::Display + Send + 'static, 29 | { 30 | println!("{msg}"); 31 | } 32 | } 33 | 34 | #[tokio::main] 35 | async fn main() -> Result<(), Box> { 36 | let my_actor_ref = MyActor::spawn(MyActor::new()); 37 | 38 | // Increment the count by 3 39 | let count = my_actor_ref.ask(Inc { amount: 3 }).await?; 40 | println!("Count is {count}"); 41 | 42 | // Increment the count by 50 in the background 43 | my_actor_ref.tell(Inc { amount: 50 }).await?; 44 | 45 | // Generic message 46 | my_actor_ref 47 | .ask(Print { 48 | msg: "Generics work!", 49 | }) 50 | .await?; 51 | 52 | // Async messages that return an Err will cause the actor to panic 53 | my_actor_ref.tell(ForceErr).await?; 54 | 55 | // Actor should be stopped, so we cannot send more messages to it 56 | assert!(my_actor_ref.ask(Inc { amount: 2 }).await.is_err()); 57 | 58 | Ok(()) 59 | } 60 | -------------------------------------------------------------------------------- /examples/message_bus.rs: -------------------------------------------------------------------------------- 1 | use kameo::prelude::*; 2 | use kameo_actors::{ 3 | message_bus::{MessageBus, Publish, Register}, 4 | DeliveryStrategy, 5 | }; 6 | 7 | #[derive(Actor)] 8 | struct MyActor; 9 | 10 | #[derive(Clone)] 11 | struct Echo { 12 | message: String, 13 | } 14 | 15 | impl Message for MyActor { 16 | type Reply = (); 17 | 18 | async fn handle(&mut self, msg: Echo, ctx: &mut Context) -> Self::Reply { 19 | println!("Actor {} says {}", ctx.actor_ref().id(), msg.message); 20 | } 21 | } 22 | 23 | #[tokio::main] 24 | async fn main() -> Result<(), Box> { 25 | let message_bus_ref = MessageBus::spawn(MessageBus::new(DeliveryStrategy::Guaranteed)); 26 | 27 | // Subscribe 28 | let my_actor_ref = MyActor::spawn(MyActor); 29 | message_bus_ref 30 | .tell(Register(my_actor_ref.clone().recipient())) 31 | .await?; 32 | 33 | let my_actor_ref2 = MyActor::spawn(MyActor); 34 | message_bus_ref 35 | .tell(Register(my_actor_ref2.clone().recipient())) 36 | .await?; 37 | 38 | // Publish 39 | message_bus_ref 40 | .tell(Publish(Echo { 41 | message: "Hola".to_string(), 42 | })) 43 | .await?; 44 | 45 | // Shutdown everything 46 | message_bus_ref.stop_gracefully().await?; 47 | message_bus_ref.wait_for_shutdown().await; 48 | 49 | my_actor_ref.stop_gracefully().await?; 50 | my_actor_ref2.stop_gracefully().await?; 51 | my_actor_ref.wait_for_shutdown().await; 52 | my_actor_ref2.wait_for_shutdown().await; 53 | 54 | Ok(()) 55 | } 56 | -------------------------------------------------------------------------------- /examples/pool.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use kameo::prelude::*; 4 | use kameo_actors::pool::{ActorPool, Broadcast, Dispatch}; 5 | 6 | #[derive(Actor, Default)] 7 | struct MyActor; 8 | 9 | struct PrintActorID; 10 | 11 | impl Message for MyActor { 12 | type Reply = (); 13 | 14 | async fn handle( 15 | &mut self, 16 | _: PrintActorID, 17 | ctx: &mut Context, 18 | ) -> Self::Reply { 19 | println!("Hello from {}", ctx.actor_ref().id()); 20 | } 21 | } 22 | 23 | #[derive(Clone)] 24 | struct ForceStop; 25 | 26 | impl Message for MyActor { 27 | type Reply = (); 28 | 29 | async fn handle(&mut self, _: ForceStop, ctx: &mut Context) -> Self::Reply { 30 | ctx.actor_ref().kill(); 31 | ctx.actor_ref().wait_for_shutdown().await; 32 | } 33 | } 34 | 35 | #[tokio::main] 36 | async fn main() -> Result<(), Box> { 37 | let pool = ActorPool::spawn(ActorPool::new(5, || MyActor::spawn(MyActor))); 38 | 39 | // Print IDs 0, 2, 4, 6, 8 40 | for _ in 0..5 { 41 | pool.tell(Dispatch(PrintActorID)).await?; 42 | } 43 | 44 | // Force all workers to stop, causing them to be restarted 45 | pool.ask(Broadcast(ForceStop)).await?; 46 | tokio::time::sleep(Duration::from_millis(200)).await; 47 | 48 | println!("Restarted all workers"); 49 | 50 | // New IDs 11, 13, 15, 17, 19 will be printed 51 | for _ in 0..5 { 52 | pool.tell(Dispatch(PrintActorID)).await?; 53 | } 54 | 55 | tokio::time::sleep(Duration::from_millis(200)).await; 56 | 57 | Ok(()) 58 | } 59 | -------------------------------------------------------------------------------- /examples/pubsub.rs: -------------------------------------------------------------------------------- 1 | use kameo::prelude::*; 2 | use kameo_actors::pubsub::{PubSub, Publish, Subscribe}; 3 | 4 | #[derive(Clone)] 5 | struct PrintActorID; 6 | 7 | #[derive(Actor, Default)] 8 | struct ActorA; 9 | 10 | impl Message for ActorA { 11 | type Reply = (); 12 | 13 | async fn handle( 14 | &mut self, 15 | _: PrintActorID, 16 | ctx: &mut Context, 17 | ) -> Self::Reply { 18 | println!("ActorA: {}", ctx.actor_ref().id()); 19 | } 20 | } 21 | 22 | #[derive(Actor, Default)] 23 | struct ActorB; 24 | 25 | impl Message for ActorB { 26 | type Reply = (); 27 | 28 | async fn handle( 29 | &mut self, 30 | _: PrintActorID, 31 | ctx: &mut Context, 32 | ) -> Self::Reply { 33 | println!("ActorB: {}", ctx.actor_ref().id()); 34 | } 35 | } 36 | 37 | #[tokio::main] 38 | async fn main() -> Result<(), Box> { 39 | let pubsub = PubSub::spawn(PubSub::::new()); 40 | 41 | let actor_a = ActorA::spawn(ActorA); 42 | let actor_b = ActorB::spawn(ActorB); 43 | 44 | pubsub.ask(Subscribe(actor_a)).await?; 45 | pubsub.ask(Subscribe(actor_b)).await?; 46 | pubsub.ask(Publish(PrintActorID)).await?; 47 | 48 | Ok(()) 49 | } 50 | -------------------------------------------------------------------------------- /examples/pubsub_filter.rs: -------------------------------------------------------------------------------- 1 | use kameo::prelude::*; 2 | use kameo_actors::pubsub::{PubSub, Publish, Subscribe, SubscribeFilter}; 3 | 4 | #[derive(Clone)] 5 | struct PrintActorID(String); 6 | 7 | #[derive(Actor, Default)] 8 | struct ActorA; 9 | 10 | impl Message for ActorA { 11 | type Reply = (); 12 | 13 | async fn handle( 14 | &mut self, 15 | PrintActorID(msg): PrintActorID, 16 | ctx: &mut Context, 17 | ) -> Self::Reply { 18 | println!("ActorA: {} - {msg}", ctx.actor_ref().id()); 19 | } 20 | } 21 | 22 | #[derive(Actor, Default)] 23 | struct ActorB; 24 | 25 | impl Message for ActorB { 26 | type Reply = (); 27 | 28 | async fn handle( 29 | &mut self, 30 | PrintActorID(msg): PrintActorID, 31 | ctx: &mut Context, 32 | ) -> Self::Reply { 33 | println!("ActorB: {} - {msg}", ctx.actor_ref().id()); 34 | } 35 | } 36 | 37 | #[derive(Actor, Default)] 38 | struct ActorC; 39 | 40 | impl Message for ActorC { 41 | type Reply = (); 42 | 43 | async fn handle( 44 | &mut self, 45 | PrintActorID(msg): PrintActorID, 46 | ctx: &mut Context, 47 | ) -> Self::Reply { 48 | println!("ActorC: {} - {msg}", ctx.actor_ref().id()); 49 | } 50 | } 51 | 52 | #[tokio::main] 53 | async fn main() -> Result<(), Box> { 54 | let pubsub = PubSub::spawn(PubSub::::new()); 55 | 56 | let actor_a = ActorA::spawn(ActorA); 57 | let actor_b = ActorB::spawn(ActorB); 58 | let actor_c = ActorC::spawn(ActorC); 59 | 60 | pubsub 61 | .ask(SubscribeFilter(actor_a, |m: &PrintActorID| { 62 | m.0.starts_with("TopicA:") 63 | })) 64 | .await?; 65 | pubsub 66 | .ask(SubscribeFilter(actor_b, |m: &PrintActorID| { 67 | m.0.starts_with("TopicB:") 68 | })) 69 | .await?; 70 | pubsub.ask(Subscribe(actor_c)).await?; 71 | 72 | pubsub 73 | .ask(Publish(PrintActorID( 74 | "TopicA: Some important note".to_string(), 75 | ))) 76 | .await?; 77 | pubsub 78 | .ask(Publish(PrintActorID( 79 | "TopicB: Some very important note".to_string(), 80 | ))) 81 | .await?; 82 | 83 | Ok(()) 84 | } 85 | -------------------------------------------------------------------------------- /examples/registry.rs: -------------------------------------------------------------------------------- 1 | use kameo::prelude::*; 2 | 3 | #[derive(Actor)] 4 | #[cfg_attr(feature = "remote", derive(kameo::RemoteActor))] 5 | pub struct MyActor; 6 | 7 | #[cfg(not(feature = "remote"))] 8 | #[tokio::main] 9 | async fn main() -> Result<(), Box> { 10 | let actor_ref = MyActor::spawn(MyActor); 11 | actor_ref.register("my awesome actor")?; 12 | 13 | let other_actor_ref = ActorRef::::lookup("my awesome actor")?.unwrap(); 14 | 15 | assert_eq!(actor_ref.id(), other_actor_ref.id()); 16 | println!("Registered and looked up actor"); 17 | 18 | Ok(()) 19 | } 20 | 21 | #[cfg(feature = "remote")] 22 | #[tokio::main] 23 | async fn main() -> Result<(), Box> { 24 | let _swarm = kameo::remote::ActorSwarm::bootstrap()?; 25 | 26 | let actor_ref = MyActor::spawn(MyActor); 27 | actor_ref.register("my awesome actor").await?; 28 | 29 | let other_actor_ref = ActorRef::::lookup("my awesome actor") 30 | .await? 31 | .unwrap(); 32 | 33 | assert_eq!(actor_ref.id(), other_actor_ref.id()); 34 | println!("Registered and looked up actor"); 35 | 36 | Ok(()) 37 | } 38 | -------------------------------------------------------------------------------- /examples/remote.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use kameo::prelude::*; 4 | use libp2p::swarm::dial_opts::DialOpts; 5 | use serde::{Deserialize, Serialize}; 6 | use tracing::{error, info}; 7 | use tracing_subscriber::EnvFilter; 8 | 9 | #[derive(Actor, RemoteActor)] 10 | pub struct MyActor { 11 | count: i64, 12 | } 13 | 14 | #[derive(Serialize, Deserialize)] 15 | pub struct Inc { 16 | amount: u32, 17 | } 18 | 19 | #[remote_message("3b9128f1-0593-44a0-b83a-f4188baa05bf")] 20 | impl Message for MyActor { 21 | type Reply = i64; 22 | 23 | async fn handle(&mut self, msg: Inc, _ctx: &mut Context) -> Self::Reply { 24 | println!("incrementing"); 25 | self.count += msg.amount as i64; 26 | self.count 27 | } 28 | } 29 | 30 | #[derive(Serialize, Deserialize)] 31 | pub struct Dec { 32 | amount: u32, 33 | } 34 | 35 | #[remote_message("20185b42-8645-47d2-8d65-2d1c68d26823")] 36 | impl Message for MyActor { 37 | type Reply = i64; 38 | 39 | async fn handle(&mut self, msg: Dec, _ctx: &mut Context) -> Self::Reply { 40 | println!("decrementing"); 41 | self.count -= msg.amount as i64; 42 | self.count 43 | } 44 | } 45 | 46 | #[tokio::main] 47 | async fn main() -> Result<(), Box> { 48 | tracing_subscriber::fmt() 49 | .with_env_filter("info".parse::()?) 50 | .without_time() 51 | .with_target(false) 52 | .init(); 53 | 54 | let is_host = match std::env::args().nth(1).as_deref() { 55 | Some("guest") => false, 56 | Some("host") => true, 57 | Some(_) | None => { 58 | error!("expected either 'host' or 'guest' argument"); 59 | return Ok(()); 60 | } 61 | }; 62 | 63 | // Bootstrap the actor swarm 64 | if is_host { 65 | ActorSwarm::bootstrap()? 66 | .listen_on("/ip4/0.0.0.0/udp/8020/quic-v1".parse()?) 67 | .await?; 68 | } else { 69 | ActorSwarm::bootstrap()?.dial( 70 | DialOpts::unknown_peer_id() 71 | .address("/ip4/0.0.0.0/udp/8020/quic-v1".parse()?) 72 | .build(), 73 | ); 74 | } 75 | 76 | if is_host { 77 | let actor_ref = MyActor::spawn(MyActor { count: 0 }); 78 | info!("registering actor"); 79 | actor_ref.register("my_actor").await?; 80 | } else { 81 | // Wait for registry to sync 82 | tokio::time::sleep(Duration::from_millis(50)).await; 83 | } 84 | 85 | loop { 86 | if !is_host { 87 | let remote_actor_ref = RemoteActorRef::::lookup("my_actor").await?; 88 | match remote_actor_ref { 89 | Some(remote_actor_ref) => { 90 | let count = remote_actor_ref.ask(&Inc { amount: 10 }).await?; 91 | println!("Incremented! Count is {count}"); 92 | } 93 | None => { 94 | println!("actor not found"); 95 | } 96 | } 97 | } 98 | 99 | tokio::time::sleep(Duration::from_secs(3)).await; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /examples/stream.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use futures::stream; 4 | use kameo::{error::Infallible, message::StreamMessage, prelude::*}; 5 | use tokio_stream::StreamExt; 6 | 7 | #[derive(Default)] 8 | pub struct MyActor { 9 | count: i64, 10 | streams_complete: u8, 11 | } 12 | 13 | impl Actor for MyActor { 14 | type Args = Self; 15 | type Error = Infallible; 16 | 17 | async fn on_start(state: Self::Args, actor_ref: ActorRef) -> Result { 18 | let stream = Box::pin(stream::repeat(1).take(5).throttle(Duration::from_secs(1))); 19 | actor_ref.attach_stream(stream, "1st stream", "1st stream"); 20 | 21 | let stream = stream::repeat(1).take(5); 22 | actor_ref.attach_stream(stream, "2nd stream", "2nd stream"); 23 | 24 | Ok(state) 25 | } 26 | } 27 | 28 | impl Message> for MyActor { 29 | type Reply = (); 30 | 31 | async fn handle( 32 | &mut self, 33 | msg: StreamMessage, 34 | ctx: &mut Context, 35 | ) -> Self::Reply { 36 | match msg { 37 | StreamMessage::Next(amount) => { 38 | self.count += amount; 39 | println!("Count is {}", self.count); 40 | } 41 | StreamMessage::Started(s) => { 42 | println!("Started {s}"); 43 | } 44 | StreamMessage::Finished(s) => { 45 | println!("Finished {s}"); 46 | self.streams_complete += 1; 47 | if self.streams_complete == 2 { 48 | ctx.actor_ref().stop_gracefully().await.unwrap(); 49 | } 50 | } 51 | } 52 | } 53 | } 54 | 55 | #[tokio::main] 56 | async fn main() { 57 | let actor_ref = MyActor::spawn(MyActor::default()); 58 | 59 | actor_ref.wait_for_shutdown().await; 60 | } 61 | -------------------------------------------------------------------------------- /macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kameo_macros" 3 | description = "Fault-tolerant Async Actors Built on Tokio macros" 4 | version = "0.16.0" 5 | readme = "../README.md" 6 | repository = "https://github.com/tqwewe/kameo" 7 | license = "MIT OR Apache-2.0" 8 | edition = "2021" 9 | 10 | [lib] 11 | proc-macro = true 12 | 13 | [dependencies] 14 | heck = "0.5" 15 | proc-macro2 = "1.0.78" 16 | quote = "1.0.35" 17 | syn = { version = "2.0.52", features = ["extra-traits", "full"] } 18 | uuid = { version = "1.10", features = ["v4"] } 19 | -------------------------------------------------------------------------------- /macros/src/derive_actor.rs: -------------------------------------------------------------------------------- 1 | use quote::{quote, ToTokens}; 2 | use syn::{ 3 | custom_keyword, 4 | parse::{Parse, ParseStream}, 5 | punctuated::Punctuated, 6 | spanned::Spanned, 7 | DeriveInput, Generics, Ident, LitStr, Token, 8 | }; 9 | 10 | pub struct DeriveActor { 11 | attrs: DeriveActorAttrs, 12 | ident: Ident, 13 | generics: Generics, 14 | } 15 | 16 | impl ToTokens for DeriveActor { 17 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 18 | let Self { 19 | attrs, 20 | ident, 21 | generics, 22 | } = self; 23 | let name = match &attrs.name { 24 | Some(s) => s.value(), 25 | None => ident.to_string(), 26 | }; 27 | let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); 28 | 29 | tokens.extend(quote! { 30 | #[automatically_derived] 31 | impl #impl_generics ::kameo::actor::Actor for #ident #ty_generics #where_clause { 32 | type Args = Self; 33 | type Error = ::kameo::error::Infallible; 34 | 35 | fn name() -> &'static str { 36 | #name 37 | } 38 | 39 | async fn on_start( 40 | state: Self::Args, 41 | _actor_ref: ::kameo::actor::ActorRef, 42 | ) -> ::std::result::Result { 43 | ::std::result::Result::Ok(state) 44 | } 45 | } 46 | }); 47 | } 48 | } 49 | 50 | impl Parse for DeriveActor { 51 | fn parse(input: ParseStream) -> syn::Result { 52 | let input: DeriveInput = input.parse()?; 53 | let ident = input.ident; 54 | let generics = input.generics; 55 | let mut attrs = None; 56 | for attr in input.attrs { 57 | if attr.path().is_ident("actor") { 58 | if attrs.is_some() { 59 | return Err(syn::Error::new( 60 | attr.span(), 61 | "actor attribute already specified", 62 | )); 63 | } 64 | attrs = Some(attr.parse_args_with(DeriveActorAttrs::parse)?); 65 | } 66 | } 67 | 68 | Ok(DeriveActor { 69 | attrs: attrs.unwrap_or_default(), 70 | ident, 71 | generics, 72 | }) 73 | } 74 | } 75 | 76 | #[derive(Default)] 77 | struct DeriveActorAttrs { 78 | name: Option, 79 | } 80 | 81 | impl Parse for DeriveActorAttrs { 82 | fn parse(input: ParseStream) -> syn::Result { 83 | #[derive(Debug)] 84 | enum Attr { 85 | Name(name, LitStr), 86 | } 87 | let attrs: Punctuated = 88 | Punctuated::parse_terminated_with(input, |input| { 89 | let lookahead = input.lookahead1(); 90 | if lookahead.peek(name) { 91 | let key: name = input.parse()?; 92 | let _: Token![=] = input.parse()?; 93 | let name: LitStr = input.parse()?; 94 | Ok(Attr::Name(key, name)) 95 | } else { 96 | Err(lookahead.error()) 97 | } 98 | })?; 99 | 100 | let mut name = None; 101 | 102 | for attr in attrs { 103 | match attr { 104 | Attr::Name(key, s) => { 105 | if name.is_none() { 106 | name = Some(s); 107 | } else { 108 | return Err(syn::Error::new(key.span, "name already set")); 109 | } 110 | } 111 | } 112 | } 113 | 114 | Ok(DeriveActorAttrs { name }) 115 | } 116 | } 117 | 118 | custom_keyword!(name); 119 | -------------------------------------------------------------------------------- /macros/src/derive_remote_actor.rs: -------------------------------------------------------------------------------- 1 | use quote::{quote, ToTokens}; 2 | use syn::{ 3 | parse::{Parse, ParseStream}, 4 | spanned::Spanned, 5 | DeriveInput, Expr, ExprAssign, ExprLit, Generics, Ident, Lit, LitStr, 6 | }; 7 | 8 | pub struct DeriveRemoteActor { 9 | attrs: DeriveRemoteActorAttrs, 10 | generics: Generics, 11 | ident: Ident, 12 | } 13 | 14 | impl ToTokens for DeriveRemoteActor { 15 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 16 | let Self { 17 | attrs, 18 | generics, 19 | ident, 20 | } = self; 21 | let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); 22 | 23 | let id = match &attrs.id { 24 | Some(id) => quote! { #id }, 25 | None => quote! { 26 | ::std::concat!(::std::module_path!(), "::", ::std::stringify!(#ident)) 27 | }, 28 | }; 29 | 30 | tokens.extend(quote! { 31 | #[automatically_derived] 32 | impl #impl_generics ::kameo::remote::RemoteActor for #ident #ty_generics #where_clause { 33 | const REMOTE_ID: &'static str = #id; 34 | } 35 | 36 | const _: () = { 37 | #[::kameo::remote::_internal::linkme::distributed_slice( 38 | ::kameo::remote::_internal::REMOTE_ACTORS 39 | )] 40 | #[linkme(crate = ::kameo::remote::_internal::linkme)] 41 | static REG: ( 42 | &'static str, 43 | ::kameo::remote::_internal::RemoteActorFns, 44 | ) = ( 45 | <#ident #ty_generics as ::kameo::remote::RemoteActor>::REMOTE_ID, 46 | ::kameo::remote::_internal::RemoteActorFns { 47 | link: ( 48 | | 49 | actor_id: ::kameo::actor::ActorID, 50 | sibbling_id: ::kameo::actor::ActorID, 51 | sibbling_remote_id: ::std::borrow::Cow<'static, str>, 52 | | { 53 | ::std::boxed::Box::pin(::kameo::remote::_internal::link::< 54 | #ident #ty_generics, 55 | >( 56 | actor_id, 57 | sibbling_id, 58 | sibbling_remote_id, 59 | )) 60 | }) as ::kameo::remote::_internal::RemoteLinkFn, 61 | unlink: ( 62 | | 63 | actor_id: ::kameo::actor::ActorID, 64 | sibbling_id: ::kameo::actor::ActorID, 65 | | { 66 | ::std::boxed::Box::pin(::kameo::remote::_internal::unlink::< 67 | #ident #ty_generics, 68 | >( 69 | actor_id, 70 | sibbling_id, 71 | )) 72 | }) as ::kameo::remote::_internal::RemoteUnlinkFn, 73 | signal_link_died: ( 74 | | 75 | dead_actor_id: ::kameo::actor::ActorID, 76 | notified_actor_id: ::kameo::actor::ActorID, 77 | stop_reason: kameo::error::ActorStopReason, 78 | | { 79 | ::std::boxed::Box::pin(::kameo::remote::_internal::signal_link_died::< 80 | #ident #ty_generics, 81 | >( 82 | dead_actor_id, 83 | notified_actor_id, 84 | stop_reason, 85 | )) 86 | }) as ::kameo::remote::_internal::RemoteSignalLinkDiedFn, 87 | }, 88 | ); 89 | }; 90 | }); 91 | } 92 | } 93 | 94 | impl Parse for DeriveRemoteActor { 95 | fn parse(input: ParseStream) -> syn::Result { 96 | let input: DeriveInput = input.parse()?; 97 | let mut attrs = None; 98 | for attr in input.attrs { 99 | if attr.path().is_ident("remote_actor") { 100 | if attrs.is_some() { 101 | return Err(syn::Error::new( 102 | attr.span(), 103 | "remote_actor attribute already specified", 104 | )); 105 | } 106 | attrs = Some(attr.parse_args_with(DeriveRemoteActorAttrs::parse)?); 107 | } 108 | } 109 | let ident = input.ident; 110 | 111 | Ok(DeriveRemoteActor { 112 | attrs: attrs.unwrap_or_default(), 113 | generics: input.generics, 114 | ident, 115 | }) 116 | } 117 | } 118 | 119 | #[derive(Default)] 120 | struct DeriveRemoteActorAttrs { 121 | id: Option, 122 | } 123 | 124 | impl Parse for DeriveRemoteActorAttrs { 125 | fn parse(input: ParseStream) -> syn::Result { 126 | let expr: ExprAssign = input 127 | .parse() 128 | .map_err(|_| syn::Error::new(input.span(), "expected id = \"...\" expression"))?; 129 | let Expr::Path(left_path) = expr.left.as_ref() else { 130 | return Err(syn::Error::new(expr.left.span(), "expected `id`")); 131 | }; 132 | if !left_path.path.is_ident("id") { 133 | return Err(syn::Error::new(expr.left.span(), "expected `id`")); 134 | } 135 | let Expr::Lit(ExprLit { 136 | lit: Lit::Str(lit_str), 137 | .. 138 | }) = *expr.right 139 | else { 140 | return Err(syn::Error::new( 141 | expr.right.span(), 142 | "expected a string literal", 143 | )); 144 | }; 145 | 146 | Ok(DeriveRemoteActorAttrs { id: Some(lit_str) }) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /macros/src/derive_reply.rs: -------------------------------------------------------------------------------- 1 | use quote::{quote, ToTokens}; 2 | use syn::{ 3 | parse::{Parse, ParseStream}, 4 | DeriveInput, Generics, Ident, 5 | }; 6 | 7 | pub struct DeriveReply { 8 | ident: Ident, 9 | generics: Generics, 10 | } 11 | 12 | impl ToTokens for DeriveReply { 13 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 14 | let Self { ident, generics } = self; 15 | let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); 16 | 17 | tokens.extend(quote! { 18 | #[automatically_derived] 19 | impl #impl_generics ::kameo::Reply for #ident #ty_generics #where_clause { 20 | type Ok = Self; 21 | type Error = ::kameo::error::Infallible; 22 | type Value = Self; 23 | 24 | #[inline] 25 | fn to_result(self) -> ::std::result::Result { 26 | ::std::result::Result::Ok(self) 27 | } 28 | 29 | #[inline] 30 | fn into_any_err(self) -> ::std::option::Option<::std::boxed::Box> { 31 | ::std::option::Option::None 32 | } 33 | 34 | #[inline] 35 | fn into_value(self) -> Self::Value { 36 | self 37 | } 38 | } 39 | }); 40 | } 41 | } 42 | 43 | impl Parse for DeriveReply { 44 | fn parse(input: ParseStream) -> syn::Result { 45 | let input: DeriveInput = input.parse()?; 46 | let ident = input.ident; 47 | let generics = input.generics; 48 | 49 | Ok(DeriveReply { ident, generics }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod derive_actor; 2 | mod derive_remote_actor; 3 | mod derive_reply; 4 | mod messages; 5 | mod remote_message; 6 | 7 | use derive_actor::DeriveActor; 8 | use derive_remote_actor::DeriveRemoteActor; 9 | use derive_reply::DeriveReply; 10 | use messages::Messages; 11 | use proc_macro::TokenStream; 12 | use quote::ToTokens; 13 | use remote_message::{RemoteMessage, RemoteMessageAttrs}; 14 | use syn::parse_macro_input; 15 | 16 | /// Attribute macro placed on `impl` blocks of actors to define messages. 17 | /// 18 | /// Methods on the impl block are marked with `#[message]`. 19 | /// This generates a struct for the message, allowing it to be sent to the actor. 20 | /// 21 | /// # Example 22 | /// 23 | /// ``` 24 | /// use kameo::messages; 25 | /// 26 | /// #[messages] 27 | /// impl Counter { 28 | /// /// Regular message 29 | /// #[message] 30 | /// pub fn inc(&mut self, amount: u32) -> i64 { 31 | /// self.count += amount as i64; 32 | /// self.count 33 | /// } 34 | /// 35 | /// /// Derives on the message 36 | /// #[message(derive(Clone, Copy))] 37 | /// pub fn dec(&self, amount: u32) { 38 | /// self.count -= amount as i64; 39 | /// } 40 | /// } 41 | /// 42 | /// counter_ref.ask(Inc { amount: 5 }).await?; 43 | /// counter_ref.ask(Dec { amount: 2 }.clone()).await?; 44 | /// ``` 45 | /// 46 | ///
47 | /// See expanded code 48 | /// 49 | /// ``` 50 | /// pub struct Inc { 51 | /// pub amount: u32, 52 | /// } 53 | /// 54 | /// impl kameo::message::Message for Counter { 55 | /// type Reply = i64; 56 | /// 57 | /// async fn handle(&mut self, msg: Counter, _ctx: &mut kameo::message::Context) -> Self::Reply { 58 | /// self.inc(msg.amount) 59 | /// } 60 | /// } 61 | /// 62 | /// pub struct Count; 63 | /// 64 | /// #[derive(Clone, Copy)] 65 | /// pub struct Dec; 66 | /// 67 | /// impl kameo::message::Message for Counter { 68 | /// type Reply = (); 69 | /// 70 | /// async fn handle(&mut self, msg: Counter, _ctx: &mut kameo::message::Context) -> Self::Reply { 71 | /// self.dec(msg.amount) 72 | /// } 73 | /// } 74 | /// ``` 75 | ///
76 | #[proc_macro_attribute] 77 | pub fn messages(_attr: TokenStream, item: TokenStream) -> TokenStream { 78 | let messages = parse_macro_input!(item as Messages); 79 | TokenStream::from(messages.into_token_stream()) 80 | } 81 | 82 | /// Derive macro implementing the [Actor](https://docs.rs/kameo/latest/kameo/actor/trait.Actor.html) trait with default behaviour. 83 | /// 84 | /// The `#[actor(name = "...")]` attribute can be specified to change the actors [Actor::name](https://docs.rs/kameo/latest/kameo/actor/trait.Actor.html#method.name). 85 | /// The default value is the actor's ident. 86 | /// 87 | /// The `#[actor(mailbox = ...)]` attribute can be specified to change the actors [Actor::Mailbox](https://docs.rs/kameo/latest/kameo/actor/trait.Actor.html#associatedtype.Mailbox). 88 | /// The values can be one of: 89 | /// - `bounded` (default capacity of 1000) 90 | /// - `bounded(64)` (custom capacity of 64) 91 | /// - `unbounded` 92 | /// 93 | /// 94 | /// # Example 95 | /// 96 | /// ``` 97 | /// use kameo::Actor; 98 | /// 99 | /// #[derive(Actor)] 100 | /// #[actor(name = "my_amazing_actor", mailbox = bounded(256))] 101 | /// struct MyActor { } 102 | /// 103 | /// assert_eq!(MyActor { }.name(), "MyActor"); 104 | /// ``` 105 | #[proc_macro_derive(Actor, attributes(actor))] 106 | pub fn derive_actor(input: TokenStream) -> TokenStream { 107 | let derive_actor = parse_macro_input!(input as DeriveActor); 108 | TokenStream::from(derive_actor.into_token_stream()) 109 | } 110 | 111 | /// Derive macro implementing the [Reply](https://docs.rs/kameo/latest/kameo/reply/trait.Reply.html) trait as an infallible reply. 112 | /// 113 | /// # Example 114 | /// 115 | /// ``` 116 | /// use kameo::Reply; 117 | /// 118 | /// #[derive(Reply)] 119 | /// struct Foo { } 120 | /// ``` 121 | #[proc_macro_derive(Reply)] 122 | pub fn derive_reply(input: TokenStream) -> TokenStream { 123 | let derive_reply = parse_macro_input!(input as DeriveReply); 124 | TokenStream::from(derive_reply.into_token_stream()) 125 | } 126 | 127 | /// Derive macro implementing the [RemoteActor](https://docs.rs/kameo/latest/kameo/actor/remote/trait.RemoteActor.html) 128 | /// trait with a default remote ID being the full path of the type being implemented. 129 | /// 130 | /// The `#[remote_actor(id = "...")]` attribute can be specified to change the default remote actor ID. 131 | /// 132 | /// # Example 133 | /// 134 | /// ``` 135 | /// use kameo::RemoteActor; 136 | /// 137 | /// #[derive(RemoteActor)] 138 | /// struct MyActor { } 139 | /// 140 | /// assert_eq!(MyActor::REMOTE_ID, "my_crate::module::MyActor"); 141 | /// ``` 142 | #[proc_macro_derive(RemoteActor, attributes(remote_actor))] 143 | pub fn derive_remote_actor(input: TokenStream) -> TokenStream { 144 | let derive_remote_actor = parse_macro_input!(input as DeriveRemoteActor); 145 | TokenStream::from(derive_remote_actor.into_token_stream()) 146 | } 147 | 148 | /// Registers an actor message to be supported with remote messages. 149 | /// 150 | /// # Example 151 | /// 152 | /// ``` 153 | /// use kameo::{remote_message, message::Message}; 154 | /// 155 | /// struct MyActor { } 156 | /// struct MyMessage { } 157 | /// 158 | /// #[remote_message("c6fa9f76-8818-4000-96f4-50c2ebd52408")] 159 | /// impl Message for MyActor { ... } 160 | /// ``` 161 | #[proc_macro_attribute] 162 | pub fn remote_message(attrs: TokenStream, input: TokenStream) -> TokenStream { 163 | let remote_actor_attrs = parse_macro_input!(attrs as RemoteMessageAttrs); 164 | let remote_actor = parse_macro_input!(input as RemoteMessage); 165 | TokenStream::from(remote_actor.into_tokens(remote_actor_attrs)) 166 | } 167 | -------------------------------------------------------------------------------- /macros/src/remote_message.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | use syn::{ 4 | parse::{Parse, ParseStream}, 5 | punctuated::Punctuated, 6 | spanned::Spanned, 7 | AngleBracketedGenericArguments, GenericArgument, Generics, ItemImpl, LitStr, PathArguments, 8 | PathSegment, Token, Type, 9 | }; 10 | use uuid::Uuid; 11 | 12 | pub struct RemoteMessageAttrs { 13 | id: LitStr, 14 | } 15 | 16 | impl Parse for RemoteMessageAttrs { 17 | fn parse(input: ParseStream) -> syn::Result { 18 | if input.is_empty() { 19 | let random_uuid = Uuid::new_v4(); 20 | return Err(syn::Error::new( 21 | input.span(), 22 | format!("expected remote message id\nhere's a random uuid you can use:\n #[remote_message(\"{random_uuid}\")]"), 23 | )); 24 | } 25 | Ok(RemoteMessageAttrs { id: input.parse()? }) 26 | } 27 | } 28 | 29 | pub struct RemoteMessage { 30 | item_impl: ItemImpl, 31 | actor_ty: Box, 32 | actor_generics: Generics, 33 | message_generics: Punctuated, 34 | } 35 | 36 | impl RemoteMessage { 37 | pub fn into_tokens(self, attrs: RemoteMessageAttrs) -> TokenStream { 38 | let RemoteMessageAttrs { id } = attrs; 39 | let Self { 40 | item_impl, 41 | actor_ty, 42 | actor_generics, 43 | message_generics, 44 | } = self; 45 | 46 | let (impl_generics, ty_generics, where_clause) = actor_generics.split_for_impl(); 47 | 48 | quote! { 49 | #item_impl 50 | 51 | #[automatically_derived] 52 | impl #impl_generics ::kameo::remote::RemoteMessage<#message_generics> for #actor_ty #ty_generics #where_clause { 53 | const REMOTE_ID: &'static str = #id; 54 | } 55 | 56 | const _: () = { 57 | #[::kameo::remote::_internal::linkme::distributed_slice( 58 | ::kameo::remote::_internal::REMOTE_MESSAGES 59 | )] 60 | #[linkme(crate = ::kameo::remote::_internal::linkme)] 61 | static REG: ( 62 | ::kameo::remote::_internal::RemoteMessageRegistrationID<'static>, 63 | ::kameo::remote::_internal::RemoteMessageFns, 64 | ) = ( 65 | ::kameo::remote::_internal::RemoteMessageRegistrationID { 66 | actor_remote_id: <#actor_ty as ::kameo::remote::RemoteActor>::REMOTE_ID, 67 | message_remote_id: <#actor_ty #ty_generics as ::kameo::remote::RemoteMessage<#message_generics>>::REMOTE_ID, 68 | }, 69 | ::kameo::remote::_internal::RemoteMessageFns { 70 | ask: (|actor_id: ::kameo::actor::ActorID, 71 | msg: ::std::vec::Vec, 72 | mailbox_timeout: ::std::option::Option<::std::time::Duration>, 73 | reply_timeout: ::std::option::Option<::std::time::Duration>| { 74 | ::std::boxed::Box::pin(::kameo::remote::_internal::ask::< 75 | #actor_ty, 76 | #message_generics, 77 | >( 78 | actor_id, 79 | msg, 80 | mailbox_timeout, 81 | reply_timeout, 82 | )) 83 | }) as ::kameo::remote::_internal::RemoteAskFn, 84 | try_ask: (|actor_id: ::kameo::actor::ActorID, 85 | msg: ::std::vec::Vec, 86 | reply_timeout: ::std::option::Option<::std::time::Duration>| { 87 | ::std::boxed::Box::pin(::kameo::remote::_internal::try_ask::< 88 | #actor_ty, 89 | #message_generics, 90 | >( 91 | actor_id, 92 | msg, 93 | reply_timeout, 94 | )) 95 | }) as ::kameo::remote::_internal::RemoteTryAskFn, 96 | tell: (|actor_id: ::kameo::actor::ActorID, 97 | msg: ::std::vec::Vec, 98 | mailbox_timeout: ::std::option::Option<::std::time::Duration>| { 99 | ::std::boxed::Box::pin(::kameo::remote::_internal::tell::< 100 | #actor_ty, 101 | #message_generics, 102 | >( 103 | actor_id, 104 | msg, 105 | mailbox_timeout, 106 | )) 107 | }) as ::kameo::remote::_internal::RemoteTellFn, 108 | try_tell: (|actor_id: ::kameo::actor::ActorID, 109 | msg: ::std::vec::Vec| { 110 | ::std::boxed::Box::pin(::kameo::remote::_internal::try_tell::< 111 | #actor_ty, 112 | #message_generics, 113 | >( 114 | actor_id, 115 | msg, 116 | )) 117 | }) as ::kameo::remote::_internal::RemoteTryTellFn, 118 | }, 119 | ); 120 | }; 121 | } 122 | } 123 | } 124 | 125 | impl Parse for RemoteMessage { 126 | fn parse(input: ParseStream) -> syn::Result { 127 | let item_impl: ItemImpl = input.parse()?; 128 | let input_span = item_impl.span(); 129 | let actor_ty = item_impl.self_ty.clone(); 130 | let actor_generics = item_impl.generics.clone(); 131 | let (_, trait_path, _) = item_impl.trait_.as_ref().ok_or_else(|| { 132 | syn::Error::new( 133 | input_span, 134 | "remote message can only be used on an impl for kameo::message::Message", 135 | ) 136 | })?; 137 | let trait_path_span = trait_path.span(); 138 | let PathSegment { 139 | ident: _, 140 | arguments, 141 | } = trait_path 142 | .segments 143 | .last() 144 | .ok_or_else(|| syn::Error::new(trait_path_span, "expected trait path"))? 145 | .clone(); 146 | let PathArguments::AngleBracketed(AngleBracketedGenericArguments { 147 | args: message_generics, 148 | .. 149 | }) = arguments 150 | else { 151 | return Err(syn::Error::new( 152 | trait_path_span, 153 | "expected angle bracket arguments", 154 | )); 155 | }; 156 | 157 | Ok(RemoteMessage { 158 | item_impl, 159 | actor_ty, 160 | actor_generics, 161 | message_generics, 162 | }) 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /scripts/release-actors.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Colors for output 4 | GREEN='\033[0;32m' 5 | RED='\033[0;31m' 6 | YELLOW='\033[1;33m' 7 | NC='\033[0m' # No Color 8 | 9 | # Function for success message 10 | success() { 11 | echo -e "${GREEN}OK${NC}" 12 | } 13 | 14 | # Function for error message and exit 15 | error() { 16 | echo -e "${RED}ERROR${NC}" 17 | echo -e "${RED}$1${NC}" 18 | exit 1 19 | } 20 | 21 | # Function to execute a command with prompts and status 22 | execute_step() { 23 | local message="$1" 24 | local command="$2" 25 | 26 | echo -e "${YELLOW}$message${NC}" 27 | read -p "Press Enter to continue... " 28 | 29 | eval "$command" 30 | if [ $? -ne 0 ]; then 31 | error "Command failed: $command" 32 | else 33 | success 34 | fi 35 | } 36 | 37 | # Check if the version argument is provided 38 | if [ -z "$1" ]; then 39 | echo "Usage: $0 " 40 | echo "Please provide the new version number as an argument." 41 | exit 1 42 | fi 43 | 44 | NEW_VERSION="$1" 45 | BRANCH=$(git rev-parse --abbrev-ref HEAD) 46 | 47 | if [ "$BRANCH" != "main" ]; then 48 | echo "Publish can only be used when checked out to \`main\` branch. You're currently checked out to \`$BRANCH\`." 49 | exit 1 50 | fi 51 | 52 | execute_step "Updating kameo_actors Cargo.toml version to $NEW_VERSION" "perl -i -pe \"s/^version = \\\".*\\\"/version = \\\"$NEW_VERSION\\\"/\" ./actors/Cargo.toml" 53 | 54 | execute_step "Publishing kameo_actors version $NEW_VERSION" "cargo publish -p kameo_actors --allow-dirty" 55 | 56 | execute_step "Creating bump git commit" "git add actors/Cargo.toml && git commit -m \"chore: bump kameo_actors to version $NEW_VERSION\"" 57 | 58 | execute_step "Pushing changes to remote" "git push origin main" 59 | 60 | execute_step "Creating git tag actors-v$NEW_VERSION" "git tag -a \"actors-v$NEW_VERSION\" -m \"Release kameo_actors v$NEW_VERSION\"" 61 | 62 | execute_step "Pushing tag to remote" "git push origin \"actors-v$NEW_VERSION\"" 63 | 64 | echo -e "${GREEN}Actors release process completed successfully!${NC}" 65 | -------------------------------------------------------------------------------- /scripts/release-kameo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Colors for output 4 | GREEN='\033[0;32m' 5 | RED='\033[0;31m' 6 | YELLOW='\033[1;33m' 7 | NC='\033[0m' # No Color 8 | 9 | # Function for success message 10 | success() { 11 | echo -e "${GREEN}OK${NC}" 12 | } 13 | 14 | # Function for error message and exit 15 | error() { 16 | echo -e "${RED}ERROR${NC}" 17 | echo -e "${RED}$1${NC}" 18 | exit 1 19 | } 20 | 21 | # Function to execute a command with prompts and status 22 | execute_step() { 23 | local message="$1" 24 | local command="$2" 25 | 26 | echo -e "${YELLOW}$message${NC}" 27 | read -p "Press Enter to continue... " 28 | 29 | eval "$command" 30 | if [ $? -ne 0 ]; then 31 | error "Command failed: $command" 32 | else 33 | success 34 | fi 35 | } 36 | 37 | # Check if the version argument is provided 38 | if [ -z "$1" ]; then 39 | echo "Usage: $0 " 40 | echo "Please provide the new version number as an argument." 41 | exit 1 42 | fi 43 | 44 | NEW_VERSION="$1" 45 | MAJOR_MINOR_VERSION="${NEW_VERSION%.*}" 46 | VERSION_LINK="${NEW_VERSION//./}" 47 | BRANCH=$(git rev-parse --abbrev-ref HEAD) 48 | 49 | if [ "$BRANCH" != "main" ]; then 50 | echo "Publish can only be used when checked out to \`main\` branch. You're currently checked out to \`$BRANCH\`." 51 | exit 1 52 | fi 53 | 54 | execute_step "Updating changelog" "git cliff --tag \"$NEW_VERSION\" --ignore-tags \"\w-\" --prepend ./CHANGELOG.md --unreleased" 55 | 56 | execute_step "Updating Cargo.toml package version to $NEW_VERSION" "perl -i -pe \"s/^version = \\\".*\\\"/version = \\\"$NEW_VERSION\\\"/\" ./Cargo.toml" 57 | 58 | execute_step "Updating README.md version to $MAJOR_MINOR_VERSION" "perl -i -pe 's/kameo = \"[^\"]*\"/kameo = \"$MAJOR_MINOR_VERSION\"/' README.md" 59 | 60 | execute_step "Updating getting-started.mdx version to $MAJOR_MINOR_VERSION" "perl -i -pe 's/kameo = \"[^\"]*\"/kameo = \"$MAJOR_MINOR_VERSION\"/' ./docs/getting-started.mdx" 61 | 62 | execute_step "Publishing kameo version $NEW_VERSION" "cargo publish -p kameo --allow-dirty" 63 | 64 | execute_step "Creating bump git commit" "git add Cargo.toml CHANGELOG.md README.md docs/getting-started.mdx && git commit -m \"chore: bump kameo to version $NEW_VERSION\"" 65 | 66 | execute_step "Pushing changes to remote" "git push origin main" 67 | 68 | execute_step "Creating git tag v$NEW_VERSION" "git tag -a \"v$NEW_VERSION\" -m \"\"" 69 | 70 | execute_step "Pushing tag to remote" "git push origin \"v$NEW_VERSION\"" 71 | 72 | execute_step "Creating CHANGELOG-Release.md for release notes" "git cliff --tag \"$NEW_VERSION\" --output ./CHANGELOG-Release.md --current" 73 | 74 | # Extract the release date from the CHANGELOG-Release.md file 75 | RELEASE_DATE=$(grep '^## \['"$NEW_VERSION"'\] - ' CHANGELOG-Release.md | sed -E 's/.* - ([0-9]{4}-[0-9]{2}-[0-9]{2})$/\1/') 76 | if [ -z "$RELEASE_DATE" ]; then 77 | error "Failed to extract release date from changelog" 78 | fi 79 | 80 | execute_step "Processing release notes" "perl -i -ne 'print if /^### / .. eof' CHANGELOG-Release.md && perl -i -ne 'print unless /^\[.*\]:.*$/ .. eof' CHANGELOG-Release.md" 81 | 82 | # Append the footer with the extracted release date 83 | echo -e "---\n\nSee the full [CHANGELOG.md](https://github.com/tqwewe/kameo/blob/main/CHANGELOG.md#${VERSION_LINK}---${RELEASE_DATE})" >> CHANGELOG-Release.md 84 | 85 | execute_step "Creating GitHub release with changelog" "gh release create \"v$NEW_VERSION\" -F CHANGELOG-Release.md" 86 | 87 | # Clean up the temporary file 88 | rm CHANGELOG-Release.md 89 | 90 | echo -e "${GREEN}Release process completed successfully!${NC}" 91 | -------------------------------------------------------------------------------- /scripts/release-macros.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Colors for output 4 | GREEN='\033[0;32m' 5 | RED='\033[0;31m' 6 | YELLOW='\033[1;33m' 7 | NC='\033[0m' # No Color 8 | 9 | # Function for success message 10 | success() { 11 | echo -e "${GREEN}OK${NC}" 12 | } 13 | 14 | # Function for error message and exit 15 | error() { 16 | echo -e "${RED}ERROR${NC}" 17 | echo -e "${RED}$1${NC}" 18 | exit 1 19 | } 20 | 21 | # Function to execute a command with prompts and status 22 | execute_step() { 23 | local message="$1" 24 | local command="$2" 25 | 26 | echo -e "${YELLOW}$message${NC}" 27 | read -p "Press Enter to continue... " 28 | 29 | eval "$command" 30 | if [ $? -ne 0 ]; then 31 | error "Command failed: $command" 32 | else 33 | success 34 | fi 35 | } 36 | 37 | # Check if the version argument is provided 38 | if [ -z "$1" ]; then 39 | echo "Usage: $0 " 40 | echo "Please provide the new version number as an argument." 41 | exit 1 42 | fi 43 | 44 | NEW_VERSION="$1" 45 | BRANCH=$(git rev-parse --abbrev-ref HEAD) 46 | 47 | if [ "$BRANCH" != "main" ]; then 48 | echo "Publish can only be used when checked out to \`main\` branch. You're currently checked out to \`$BRANCH\`." 49 | exit 1 50 | fi 51 | 52 | execute_step "Updating kameo_macros Cargo.toml version to $NEW_VERSION" "perl -i -pe \"s/^version = \\\".*\\\"/version = \\\"$NEW_VERSION\\\"/\" ./macros/Cargo.toml" 53 | 54 | execute_step "Updating kameo_macros version dependency in kameo Cargo.toml" "perl -i -pe \"s/kameo_macros = { version = \\\"[^\\\"]*\\\"/kameo_macros = { version = \\\"$NEW_VERSION\\\"/\" ./Cargo.toml" 55 | 56 | execute_step "Publishing kameo_macros version $NEW_VERSION" "cargo publish -p kameo_macros --allow-dirty" 57 | 58 | execute_step "Creating bump git commit" "git add macros/Cargo.toml Cargo.toml && git commit -m \"chore: bump kameo_macros to version $NEW_VERSION\"" 59 | 60 | execute_step "Pushing changes to remote" "git push origin main" 61 | 62 | execute_step "Creating git tag macros-v$NEW_VERSION" "git tag -a \"macros-v$NEW_VERSION\" -m \"Release kameo_macros v$NEW_VERSION\"" 63 | 64 | execute_step "Pushing tag to remote" "git push origin \"macros-v$NEW_VERSION\"" 65 | 66 | echo -e "${GREEN}Macros release process completed successfully!${NC}" 67 | -------------------------------------------------------------------------------- /src/actor/kind.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::VecDeque, mem, ops::ControlFlow, panic::AssertUnwindSafe}; 2 | 3 | use futures::{Future, FutureExt}; 4 | 5 | use crate::{ 6 | actor::{Actor, ActorRef, WeakActorRef}, 7 | error::{ActorStopReason, PanicError}, 8 | mailbox::{MailboxReceiver, Signal}, 9 | message::BoxMessage, 10 | reply::BoxReplySender, 11 | }; 12 | 13 | use super::ActorID; 14 | 15 | pub(crate) trait ActorState: Sized { 16 | fn new_from_actor(actor: A, actor_ref: WeakActorRef) -> Self; 17 | 18 | fn next( 19 | &mut self, 20 | mailbox_rx: &mut MailboxReceiver, 21 | ) -> impl Future>> + Send; 22 | 23 | fn handle_startup_finished( 24 | &mut self, 25 | ) -> impl Future> + Send; 26 | 27 | fn handle_message( 28 | &mut self, 29 | message: BoxMessage, 30 | actor_ref: ActorRef, 31 | reply: Option, 32 | sent_within_actor: bool, 33 | ) -> impl Future> + Send; 34 | 35 | fn handle_link_died( 36 | &mut self, 37 | id: ActorID, 38 | reason: ActorStopReason, 39 | ) -> impl Future> + Send; 40 | 41 | fn handle_stop(&mut self) -> impl Future> + Send; 42 | 43 | fn on_shutdown( 44 | &mut self, 45 | reason: ActorStopReason, 46 | ) -> impl Future> + Send; 47 | 48 | fn shutdown(self) -> impl Future + Send; 49 | } 50 | 51 | pub(crate) struct ActorBehaviour { 52 | actor_ref: WeakActorRef, 53 | state: A, 54 | finished_startup: bool, 55 | startup_buffer: VecDeque>, 56 | } 57 | 58 | impl ActorState for ActorBehaviour 59 | where 60 | A: Actor, 61 | { 62 | #[inline] 63 | fn new_from_actor(actor: A, actor_ref: WeakActorRef) -> Self { 64 | ActorBehaviour { 65 | actor_ref, 66 | state: actor, 67 | finished_startup: false, 68 | startup_buffer: VecDeque::new(), 69 | } 70 | } 71 | 72 | async fn next(&mut self, mailbox_rx: &mut MailboxReceiver) -> Option> { 73 | self.state.next(self.actor_ref.clone(), mailbox_rx).await 74 | } 75 | 76 | async fn handle_startup_finished(&mut self) -> ControlFlow { 77 | self.finished_startup = true; 78 | for signal in mem::take(&mut self.startup_buffer).drain(..) { 79 | match signal { 80 | Signal::Message { 81 | message, 82 | actor_ref, 83 | reply, 84 | sent_within_actor, 85 | } => { 86 | self.handle_message(message, actor_ref, reply, sent_within_actor) 87 | .await?; 88 | } 89 | _ => unreachable!(), 90 | } 91 | } 92 | 93 | ControlFlow::Continue(()) 94 | } 95 | 96 | #[inline] 97 | async fn handle_message( 98 | &mut self, 99 | message: BoxMessage, 100 | actor_ref: ActorRef, 101 | reply: Option, 102 | sent_within_actor: bool, 103 | ) -> ControlFlow { 104 | if !sent_within_actor && !self.finished_startup { 105 | // The actor is still starting up, so we'll push this message to a buffer to be processed upon startup 106 | self.startup_buffer.push_back(Signal::Message { 107 | message, 108 | actor_ref, 109 | reply, 110 | sent_within_actor, 111 | }); 112 | return ControlFlow::Continue(()); 113 | } 114 | 115 | let res = AssertUnwindSafe(message.handle_dyn(&mut self.state, actor_ref, reply)) 116 | .catch_unwind() 117 | .await; 118 | match res { 119 | Ok(Ok(())) => ControlFlow::Continue(()), 120 | Ok(Err(err)) => ControlFlow::Break(ActorStopReason::Panicked(PanicError::new(err))), // The reply was an error 121 | Err(err) => ControlFlow::Break(ActorStopReason::Panicked( 122 | PanicError::new_from_panic_any(err), 123 | )), // The handler panicked 124 | } 125 | } 126 | 127 | #[inline] 128 | async fn handle_link_died( 129 | &mut self, 130 | id: ActorID, 131 | reason: ActorStopReason, 132 | ) -> ControlFlow { 133 | let res = AssertUnwindSafe(self.state.on_link_died( 134 | self.actor_ref.clone(), 135 | id, 136 | reason.clone(), 137 | )) 138 | .catch_unwind() 139 | .await; 140 | self.actor_ref.links.lock().await.remove(&id); 141 | match res { 142 | Ok(Ok(flow)) => flow, 143 | Ok(Err(err)) => { 144 | ControlFlow::Break(ActorStopReason::Panicked(PanicError::new(Box::new(err)))) 145 | } 146 | Err(err) => ControlFlow::Break(ActorStopReason::Panicked( 147 | PanicError::new_from_panic_any(err), 148 | )), 149 | } 150 | } 151 | 152 | #[inline] 153 | async fn handle_stop(&mut self) -> ControlFlow { 154 | match self.handle_startup_finished().await { 155 | ControlFlow::Continue(_) => ControlFlow::Break(ActorStopReason::Normal), 156 | ControlFlow::Break(reason) => ControlFlow::Break(reason), 157 | } 158 | } 159 | 160 | #[inline] 161 | async fn on_shutdown(&mut self, reason: ActorStopReason) -> ControlFlow { 162 | match reason { 163 | ActorStopReason::Normal => ControlFlow::Break(ActorStopReason::Normal), 164 | ActorStopReason::Killed => ControlFlow::Break(ActorStopReason::Killed), 165 | ActorStopReason::Panicked(err) => { 166 | match self.state.on_panic(self.actor_ref.clone(), err).await { 167 | Ok(ControlFlow::Continue(())) => ControlFlow::Continue(()), 168 | Ok(ControlFlow::Break(reason)) => ControlFlow::Break(reason), 169 | Err(err) => ControlFlow::Break(ActorStopReason::Panicked(PanicError::new( 170 | Box::new(err), 171 | ))), 172 | } 173 | } 174 | ActorStopReason::LinkDied { id, reason } => { 175 | ControlFlow::Break(ActorStopReason::LinkDied { id, reason }) 176 | } 177 | #[cfg(feature = "remote")] 178 | ActorStopReason::PeerDisconnected => { 179 | ControlFlow::Break(ActorStopReason::PeerDisconnected) 180 | } 181 | } 182 | } 183 | 184 | #[inline] 185 | async fn shutdown(self) -> A { 186 | self.state 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | #![warn(missing_docs)] 3 | #![warn(clippy::all)] 4 | #![warn(rust_2018_idioms)] 5 | #![warn(missing_debug_implementations)] 6 | #![deny(unused_must_use)] 7 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 8 | 9 | pub mod actor; 10 | pub mod error; 11 | pub mod mailbox; 12 | pub mod message; 13 | #[cfg(not(feature = "remote"))] 14 | pub mod registry; 15 | #[cfg(feature = "remote")] 16 | pub mod remote; 17 | pub mod reply; 18 | pub mod request; 19 | 20 | pub use actor::Actor; 21 | #[cfg(feature = "macros")] 22 | pub use kameo_macros::{messages, remote_message, Actor, RemoteActor, Reply}; 23 | pub use reply::Reply; 24 | 25 | /// Commonly used types and functions that can be imported with a single use statement. 26 | /// 27 | /// ``` 28 | /// use kameo::prelude::*; 29 | /// ``` 30 | /// 31 | /// This module includes the most essential actor components, messaging types, 32 | /// and traits needed for typical actor system usage. 33 | pub mod prelude { 34 | #[cfg(feature = "macros")] 35 | pub use kameo_macros::{messages, remote_message, Actor, RemoteActor, Reply}; 36 | 37 | #[cfg(feature = "remote")] 38 | pub use crate::actor::RemoteActorRef; 39 | pub use crate::actor::{ 40 | Actor, ActorID, ActorRef, PreparedActor, Recipient, WeakActorRef, WeakRecipient, 41 | }; 42 | #[cfg(feature = "remote")] 43 | pub use crate::error::RemoteSendError; 44 | pub use crate::error::{ActorStopReason, PanicError, SendError}; 45 | pub use crate::mailbox::{self, MailboxReceiver, MailboxSender}; 46 | pub use crate::message::{Context, Message}; 47 | #[cfg(feature = "remote")] 48 | pub use crate::remote::{ActorSwarm, RemoteActor, RemoteMessage}; 49 | pub use crate::reply::{DelegatedReply, ForwardedReply, Reply, ReplyError, ReplySender}; 50 | } 51 | -------------------------------------------------------------------------------- /src/registry.rs: -------------------------------------------------------------------------------- 1 | //! Local actor registry for registering and looking up actors by arbitrary names. 2 | 3 | use std::{ 4 | any::Any, 5 | borrow::{Borrow, Cow}, 6 | collections::{hash_map::Keys, HashMap}, 7 | hash::Hash, 8 | sync::{Arc, Mutex}, 9 | }; 10 | 11 | use once_cell::sync::Lazy; 12 | 13 | use crate::{actor::ActorRef, error::RegistryError, Actor}; 14 | 15 | /// Global actor registry for local actors. 16 | pub static ACTOR_REGISTRY: Lazy>> = 17 | Lazy::new(|| Arc::new(Mutex::new(ActorRegistry::new()))); 18 | 19 | type AnyActorRef = Box; 20 | 21 | /// A local actor registry storing actor refs by name. 22 | #[derive(Debug)] 23 | pub struct ActorRegistry { 24 | actor_refs: HashMap, AnyActorRef>, 25 | } 26 | 27 | impl ActorRegistry { 28 | /// Creates a new empty actor registry. 29 | pub fn new() -> Self { 30 | ActorRegistry { 31 | actor_refs: HashMap::new(), 32 | } 33 | } 34 | 35 | /// Creates a new empty actor registry with at least the specified capacity. 36 | pub fn with_capacity(capacity: usize) -> Self { 37 | ActorRegistry { 38 | actor_refs: HashMap::with_capacity(capacity), 39 | } 40 | } 41 | 42 | /// Returns the number of actor refs that can be held without reallocating. 43 | pub fn capacity(&self) -> usize { 44 | self.actor_refs.capacity() 45 | } 46 | 47 | /// An iterator visiting all registered actor refs in arbitrary order. 48 | pub fn names(&self) -> Keys<'_, Cow<'static, str>, AnyActorRef> { 49 | self.actor_refs.keys() 50 | } 51 | 52 | /// The number of registered actor refs. 53 | pub fn len(&self) -> usize { 54 | self.actor_refs.len() 55 | } 56 | 57 | /// Returns `true` if the registry contains no actor refs. 58 | pub fn is_empty(&self) -> bool { 59 | self.actor_refs.is_empty() 60 | } 61 | 62 | /// Clears the registry, removing all actor refs. Keeps the allocated memory for reuse. 63 | pub fn clear(&mut self) { 64 | self.actor_refs.clear() 65 | } 66 | 67 | /// Gets an actor ref previously registered for a given actor type. 68 | /// 69 | /// If the actor type does not match the one it was registered with, 70 | /// a [`RegistryError::BadActorType`] error will be returned. 71 | pub fn get(&mut self, name: &Q) -> Result>, RegistryError> 72 | where 73 | A: Actor, 74 | Q: Hash + Eq + ?Sized, 75 | Cow<'static, str>: Borrow, 76 | { 77 | self.actor_refs 78 | .get(name) 79 | .map(|actor_ref| { 80 | actor_ref 81 | .downcast_ref() 82 | .cloned() 83 | .ok_or(RegistryError::BadActorType) 84 | }) 85 | .transpose() 86 | } 87 | 88 | /// Returns `true` if an actor has been registered under a given name. 89 | pub fn contains_name(&self, name: &Q) -> bool 90 | where 91 | Q: Hash + Eq + ?Sized, 92 | Cow<'static, str>: Borrow, 93 | { 94 | self.actor_refs.contains_key(name) 95 | } 96 | 97 | /// Inserts a new actor ref under a given name, which can be used later to be looked up. 98 | pub fn insert( 99 | &mut self, 100 | name: impl Into>, 101 | actor_ref: ActorRef, 102 | ) -> bool { 103 | self.actor_refs 104 | .insert(name.into(), Box::new(actor_ref)) 105 | .is_some() 106 | } 107 | 108 | /// Removes a previously registered actor ref under a given name. 109 | pub fn remove(&mut self, name: &Q) -> bool 110 | where 111 | Q: Hash + Eq + ?Sized, 112 | Cow<'static, str>: Borrow, 113 | { 114 | self.actor_refs.remove(name).is_some() 115 | } 116 | } 117 | 118 | impl Default for ActorRegistry { 119 | fn default() -> Self { 120 | Self::new() 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/remote.rs: -------------------------------------------------------------------------------- 1 | //! # Remote Actors in Kameo 2 | //! 3 | //! The `remote` module in Kameo provides tools for managing distributed actors across nodes, 4 | //! enabling actors to communicate seamlessly in a peer-to-peer (P2P) network. By leveraging 5 | //! the [libp2p](https://libp2p.io) library, Kameo allows you to register actors under unique 6 | //! names and send messages between actors on different nodes as though they were local. 7 | //! 8 | //! ## Key Features 9 | //! 10 | //! - **Swarm Management**: The `ActorSwarm` struct handles a distributed swarm of nodes, 11 | //! managing peer discovery and communication. 12 | //! - **Actor Registration**: Actors can be registered under a unique name and looked up across 13 | //! the network using the `RemoteActorRef`. 14 | //! - **Message Routing**: Ensures reliable message delivery between nodes using a combination 15 | //! of Kademlia DHT and libp2p's networking capabilities. 16 | //! 17 | //! ## Getting Started 18 | //! 19 | //! To use remote actors, you must first initialize an `ActorSwarm`, which will set up the necessary 20 | //! networking components to allow remote actors to communicate across nodes. 21 | //! 22 | //! ``` 23 | //! use kameo::remote::ActorSwarm; 24 | //! 25 | //! # tokio_test::block_on(async { 26 | //! // Initialize the actor swarm 27 | //! ActorSwarm::bootstrap()? 28 | //! .listen_on("/ip4/0.0.0.0/udp/8020/quic-v1".parse()?).await?; 29 | //! # Ok::<(), Box>(()) 30 | //! # }); 31 | //! ``` 32 | //! 33 | //! ## Example Use Case 34 | //! 35 | //! - A distributed chat system where actors represent individual users, and messages are sent between them across multiple nodes. 36 | //! 37 | //! ## Types in the Module 38 | //! 39 | //! - [`ActorSwarm`]: The core struct for managing the distributed swarm of nodes and coordinating actor registration and messaging. 40 | //! - [`SwarmFuture`]: A future that holds the response from the actor swarm. 41 | //! - [`RemoteActor`]: A trait for identifying remote actors via a unique ID. 42 | //! - [`RemoteMessage`]: A trait for identifying remote messages via a unique ID. 43 | //! 44 | //! ### Re-exports 45 | //! 46 | //! - `Keypair`, `PeerId`, `dial_opts`: Re-exported from the libp2p library to assist with handling peer identities and dialing options. 47 | 48 | use std::{ 49 | any, 50 | borrow::Cow, 51 | collections::{HashMap, HashSet}, 52 | time::Duration, 53 | }; 54 | 55 | use _internal::{ 56 | RemoteActorFns, RemoteMessageFns, RemoteMessageRegistrationID, REMOTE_ACTORS, REMOTE_MESSAGES, 57 | }; 58 | pub use libp2p::swarm::dial_opts; 59 | pub use libp2p::PeerId; 60 | pub use libp2p_identity::Keypair; 61 | use once_cell::sync::Lazy; 62 | use tokio::sync::Mutex; 63 | 64 | use crate::{ 65 | actor::{ActorID, Links}, 66 | error::{ActorStopReason, Infallible, RemoteSendError}, 67 | mailbox::SignalMailbox, 68 | }; 69 | 70 | #[doc(hidden)] 71 | pub mod _internal; 72 | mod swarm; 73 | 74 | pub use swarm::*; 75 | 76 | pub(crate) static REMOTE_REGISTRY: Lazy>> = 77 | Lazy::new(|| Mutex::new(HashMap::new())); 78 | 79 | pub(crate) struct RemoteRegistryActorRef { 80 | pub(crate) actor_ref: Box, 81 | pub(crate) signal_mailbox: Box, 82 | pub(crate) links: Links, 83 | } 84 | 85 | static REMOTE_ACTORS_MAP: Lazy> = Lazy::new(|| { 86 | let mut existing_ids = HashSet::new(); 87 | for (id, _) in REMOTE_ACTORS { 88 | if !existing_ids.insert(id) { 89 | panic!("duplicate remote actor detected for actor '{id}'"); 90 | } 91 | } 92 | REMOTE_ACTORS.iter().copied().collect() 93 | }); 94 | 95 | static REMOTE_MESSAGES_MAP: Lazy, RemoteMessageFns>> = 96 | Lazy::new(|| { 97 | let mut existing_ids = HashSet::new(); 98 | for (id, _) in REMOTE_MESSAGES { 99 | if !existing_ids.insert(id) { 100 | panic!( 101 | "duplicate remote message detected for actor '{}' and message '{}'", 102 | id.actor_remote_id, id.message_remote_id 103 | ); 104 | } 105 | } 106 | REMOTE_MESSAGES.iter().copied().collect() 107 | }); 108 | 109 | /// `RemoteActor` is a trait for identifying actors remotely. 110 | /// 111 | /// Each remote actor must implement this trait and provide a unique identifier string (`REMOTE_ID`). 112 | /// The identifier is essential to distinguish between different actor types during remote communication. 113 | /// 114 | /// ## Example with Derive 115 | /// 116 | /// ``` 117 | /// use kameo::{Actor, RemoteActor}; 118 | /// 119 | /// #[derive(Actor, RemoteActor)] 120 | /// pub struct MyActor; 121 | /// ``` 122 | /// 123 | /// ## Example Manual Implementation 124 | /// 125 | /// ``` 126 | /// use kameo::remote::RemoteActor; 127 | /// 128 | /// pub struct MyActor; 129 | /// 130 | /// impl RemoteActor for MyActor { 131 | /// const REMOTE_ID: &'static str = "my_actor_id"; 132 | /// } 133 | /// ``` 134 | pub trait RemoteActor { 135 | /// The remote identifier string. 136 | const REMOTE_ID: &'static str; 137 | } 138 | 139 | /// `RemoteMessage` is a trait for identifying messages that are sent between remote actors. 140 | /// 141 | /// Each remote message type must implement this trait and provide a unique identifier string (`REMOTE_ID`). 142 | /// The unique ID ensures that each message type is recognized correctly during message passing between nodes. 143 | /// 144 | /// This trait is typically implemented automatically with the [`#[remote_message]`](crate::remote_message) macro. 145 | pub trait RemoteMessage { 146 | /// The remote identifier string. 147 | const REMOTE_ID: &'static str; 148 | } 149 | 150 | pub(crate) async fn ask( 151 | actor_id: ActorID, 152 | actor_remote_id: Cow<'static, str>, 153 | message_remote_id: Cow<'static, str>, 154 | payload: Vec, 155 | mailbox_timeout: Option, 156 | reply_timeout: Option, 157 | immediate: bool, 158 | ) -> Result, RemoteSendError>> { 159 | let Some(fns) = REMOTE_MESSAGES_MAP.get(&RemoteMessageRegistrationID { 160 | actor_remote_id: &actor_remote_id, 161 | message_remote_id: &message_remote_id, 162 | }) else { 163 | return Err(RemoteSendError::UnknownMessage { 164 | actor_remote_id, 165 | message_remote_id, 166 | }); 167 | }; 168 | if immediate { 169 | (fns.try_ask)(actor_id, payload, reply_timeout).await 170 | } else { 171 | (fns.ask)(actor_id, payload, mailbox_timeout, reply_timeout).await 172 | } 173 | } 174 | 175 | pub(crate) async fn tell( 176 | actor_id: ActorID, 177 | actor_remote_id: Cow<'static, str>, 178 | message_remote_id: Cow<'static, str>, 179 | payload: Vec, 180 | mailbox_timeout: Option, 181 | immediate: bool, 182 | ) -> Result<(), RemoteSendError> { 183 | let Some(fns) = REMOTE_MESSAGES_MAP.get(&RemoteMessageRegistrationID { 184 | actor_remote_id: &actor_remote_id, 185 | message_remote_id: &message_remote_id, 186 | }) else { 187 | return Err(RemoteSendError::UnknownMessage { 188 | actor_remote_id, 189 | message_remote_id, 190 | }); 191 | }; 192 | if immediate { 193 | (fns.try_tell)(actor_id, payload).await 194 | } else { 195 | (fns.tell)(actor_id, payload, mailbox_timeout).await 196 | } 197 | } 198 | 199 | pub(crate) async fn link( 200 | actor_id: ActorID, 201 | actor_remote_id: Cow<'static, str>, 202 | sibbling_id: ActorID, 203 | sibbling_remote_id: Cow<'static, str>, 204 | ) -> Result<(), RemoteSendError> { 205 | let Some(fns) = REMOTE_ACTORS_MAP.get(&*actor_remote_id) else { 206 | return Err(RemoteSendError::UnknownActor { actor_remote_id }); 207 | }; 208 | 209 | (fns.link)(actor_id, sibbling_id, sibbling_remote_id).await 210 | } 211 | 212 | pub(crate) async fn unlink( 213 | actor_id: ActorID, 214 | actor_remote_id: Cow<'static, str>, 215 | sibbling_id: ActorID, 216 | ) -> Result<(), RemoteSendError> { 217 | let Some(fns) = REMOTE_ACTORS_MAP.get(&*actor_remote_id) else { 218 | return Err(RemoteSendError::UnknownActor { actor_remote_id }); 219 | }; 220 | 221 | (fns.unlink)(actor_id, sibbling_id).await 222 | } 223 | 224 | pub(crate) async fn signal_link_died( 225 | dead_actor_id: ActorID, 226 | notified_actor_id: ActorID, 227 | notified_actor_remote_id: Cow<'static, str>, 228 | stop_reason: ActorStopReason, 229 | ) -> Result<(), RemoteSendError> { 230 | let Some(fns) = REMOTE_ACTORS_MAP.get(&*notified_actor_remote_id) else { 231 | return Err(RemoteSendError::UnknownActor { 232 | actor_remote_id: notified_actor_remote_id, 233 | }); 234 | }; 235 | 236 | (fns.signal_link_died)(dead_actor_id, notified_actor_id, stop_reason).await 237 | } 238 | -------------------------------------------------------------------------------- /src/remote/_internal.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::time::Duration; 3 | 4 | use futures::future::BoxFuture; 5 | pub use linkme; 6 | use serde::de::DeserializeOwned; 7 | use serde::Serialize; 8 | 9 | use crate::actor::{ActorID, ActorRef, Link}; 10 | use crate::error::{ActorStopReason, Infallible, RemoteSendError}; 11 | use crate::message::Message; 12 | use crate::{Actor, Reply}; 13 | 14 | use super::REMOTE_REGISTRY; 15 | 16 | #[linkme::distributed_slice] 17 | pub static REMOTE_ACTORS: [(&'static str, RemoteActorFns)]; 18 | 19 | #[linkme::distributed_slice] 20 | pub static REMOTE_MESSAGES: [(RemoteMessageRegistrationID<'static>, RemoteMessageFns)]; 21 | 22 | #[derive(Clone, Copy, Debug)] 23 | pub struct RemoteActorFns { 24 | pub link: RemoteLinkFn, 25 | pub unlink: RemoteUnlinkFn, 26 | pub signal_link_died: RemoteSignalLinkDiedFn, 27 | } 28 | 29 | #[derive(Clone, Copy, Debug)] 30 | pub struct RemoteMessageFns { 31 | pub ask: RemoteAskFn, 32 | pub try_ask: RemoteTryAskFn, 33 | pub tell: RemoteTellFn, 34 | pub try_tell: RemoteTryTellFn, 35 | } 36 | 37 | pub type RemoteAskFn = fn( 38 | actor_id: ActorID, 39 | msg: Vec, 40 | mailbox_timeout: Option, 41 | reply_timeout: Option, 42 | ) -> BoxFuture<'static, Result, RemoteSendError>>>; 43 | 44 | pub type RemoteTryAskFn = fn( 45 | actor_id: ActorID, 46 | msg: Vec, 47 | reply_timeout: Option, 48 | ) -> BoxFuture<'static, Result, RemoteSendError>>>; 49 | 50 | pub type RemoteTellFn = fn( 51 | actor_id: ActorID, 52 | msg: Vec, 53 | mailbox_timeout: Option, 54 | ) -> BoxFuture<'static, Result<(), RemoteSendError>>; 55 | 56 | pub type RemoteTryTellFn = 57 | fn(actor_id: ActorID, msg: Vec) -> BoxFuture<'static, Result<(), RemoteSendError>>; 58 | 59 | pub type RemoteLinkFn = fn( 60 | actor_id: ActorID, 61 | sibbling_id: ActorID, 62 | sibbling_remote_id: Cow<'static, str>, 63 | ) -> BoxFuture<'static, Result<(), RemoteSendError>>; 64 | 65 | pub type RemoteUnlinkFn = fn( 66 | actor_id: ActorID, 67 | sibbling_id: ActorID, 68 | ) -> BoxFuture<'static, Result<(), RemoteSendError>>; 69 | 70 | pub type RemoteSignalLinkDiedFn = fn( 71 | dead_actor_id: ActorID, 72 | notified_actor_id: ActorID, 73 | stop_reason: ActorStopReason, 74 | ) 75 | -> BoxFuture<'static, Result<(), RemoteSendError>>; 76 | 77 | #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] 78 | pub struct RemoteMessageRegistrationID<'a> { 79 | pub actor_remote_id: &'a str, 80 | pub message_remote_id: &'a str, 81 | } 82 | 83 | pub async fn ask( 84 | actor_id: ActorID, 85 | msg: Vec, 86 | mailbox_timeout: Option, 87 | reply_timeout: Option, 88 | ) -> Result, RemoteSendError>> 89 | where 90 | A: Actor + Message, 91 | M: DeserializeOwned + Send + 'static, 92 | ::Ok: Serialize, 93 | ::Error: Serialize, 94 | { 95 | let actor_ref = { 96 | let remote_actors = REMOTE_REGISTRY.lock().await; 97 | remote_actors 98 | .get(&actor_id) 99 | .ok_or(RemoteSendError::ActorNotRunning)? 100 | .actor_ref 101 | .downcast_ref::>() 102 | .ok_or(RemoteSendError::BadActorType)? 103 | .clone() 104 | }; 105 | let msg: M = rmp_serde::decode::from_slice(&msg) 106 | .map_err(|err| RemoteSendError::DeserializeMessage(err.to_string()))?; 107 | 108 | let res = actor_ref 109 | .ask(msg) 110 | .mailbox_timeout_opt(mailbox_timeout) 111 | .reply_timeout_opt(reply_timeout) 112 | .send() 113 | .await; 114 | match res { 115 | Ok(reply) => Ok(rmp_serde::to_vec_named(&reply) 116 | .map_err(|err| RemoteSendError::SerializeReply(err.to_string()))?), 117 | Err(err) => Err(RemoteSendError::from(err) 118 | .map_err(|err| match rmp_serde::to_vec_named(&err) { 119 | Ok(payload) => RemoteSendError::HandlerError(payload), 120 | Err(err) => RemoteSendError::SerializeHandlerError(err.to_string()), 121 | }) 122 | .flatten()), 123 | } 124 | } 125 | 126 | pub async fn try_ask( 127 | actor_id: ActorID, 128 | msg: Vec, 129 | reply_timeout: Option, 130 | ) -> Result, RemoteSendError>> 131 | where 132 | A: Actor + Message, 133 | M: DeserializeOwned + Send + 'static, 134 | ::Ok: Serialize, 135 | ::Error: Serialize, 136 | { 137 | let actor_ref = { 138 | let remote_actors = REMOTE_REGISTRY.lock().await; 139 | remote_actors 140 | .get(&actor_id) 141 | .ok_or(RemoteSendError::ActorNotRunning)? 142 | .actor_ref 143 | .downcast_ref::>() 144 | .ok_or(RemoteSendError::BadActorType)? 145 | .clone() 146 | }; 147 | let msg: M = rmp_serde::decode::from_slice(&msg) 148 | .map_err(|err| RemoteSendError::DeserializeMessage(err.to_string()))?; 149 | 150 | let res = actor_ref 151 | .ask(msg) 152 | .reply_timeout_opt(reply_timeout) 153 | .try_send() 154 | .await; 155 | match res { 156 | Ok(reply) => Ok(rmp_serde::to_vec_named(&reply) 157 | .map_err(|err| RemoteSendError::SerializeReply(err.to_string()))?), 158 | Err(err) => Err(RemoteSendError::from(err) 159 | .map_err(|err| match rmp_serde::to_vec_named(&err) { 160 | Ok(payload) => RemoteSendError::HandlerError(payload), 161 | Err(err) => RemoteSendError::SerializeHandlerError(err.to_string()), 162 | }) 163 | .flatten()), 164 | } 165 | } 166 | 167 | pub async fn tell( 168 | actor_id: ActorID, 169 | msg: Vec, 170 | mailbox_timeout: Option, 171 | ) -> Result<(), RemoteSendError> 172 | where 173 | A: Actor + Message, 174 | M: DeserializeOwned + Send + 'static, 175 | { 176 | let actor_ref = { 177 | let remote_actors = REMOTE_REGISTRY.lock().await; 178 | remote_actors 179 | .get(&actor_id) 180 | .ok_or(RemoteSendError::ActorNotRunning)? 181 | .actor_ref 182 | .downcast_ref::>() 183 | .ok_or(RemoteSendError::BadActorType)? 184 | .clone() 185 | }; 186 | let msg: M = rmp_serde::decode::from_slice(&msg) 187 | .map_err(|err| RemoteSendError::DeserializeMessage(err.to_string()))?; 188 | 189 | let res = actor_ref 190 | .tell(msg) 191 | .mailbox_timeout_opt(mailbox_timeout) 192 | .send() 193 | .await; 194 | match res { 195 | Ok(()) => Ok(()), 196 | Err(err) => Err(RemoteSendError::from(err)), 197 | } 198 | } 199 | 200 | pub async fn try_tell(actor_id: ActorID, msg: Vec) -> Result<(), RemoteSendError> 201 | where 202 | A: Actor + Message, 203 | M: DeserializeOwned + Send + 'static, 204 | { 205 | let actor_ref = { 206 | let remote_actors = REMOTE_REGISTRY.lock().await; 207 | remote_actors 208 | .get(&actor_id) 209 | .ok_or(RemoteSendError::ActorNotRunning)? 210 | .actor_ref 211 | .downcast_ref::>() 212 | .ok_or(RemoteSendError::BadActorType)? 213 | .clone() 214 | }; 215 | let msg: M = rmp_serde::decode::from_slice(&msg) 216 | .map_err(|err| RemoteSendError::DeserializeMessage(err.to_string()))?; 217 | 218 | let res = actor_ref.tell(msg).try_send(); 219 | match res { 220 | Ok(()) => Ok(()), 221 | Err(err) => Err(RemoteSendError::from(err)), 222 | } 223 | } 224 | 225 | pub async fn link( 226 | actor_id: ActorID, 227 | sibbling_id: ActorID, 228 | sibbling_remote_id: Cow<'static, str>, 229 | ) -> Result<(), RemoteSendError> 230 | where 231 | A: Actor, 232 | { 233 | let actor_ref = { 234 | let remote_actors = REMOTE_REGISTRY.lock().await; 235 | remote_actors 236 | .get(&actor_id) 237 | .ok_or(RemoteSendError::ActorNotRunning)? 238 | .actor_ref 239 | .downcast_ref::>() 240 | .ok_or(RemoteSendError::BadActorType)? 241 | .clone() 242 | }; 243 | 244 | actor_ref 245 | .links 246 | .lock() 247 | .await 248 | .insert(sibbling_id, Link::Remote(sibbling_remote_id)); 249 | 250 | Ok(()) 251 | } 252 | 253 | pub async fn unlink( 254 | actor_id: ActorID, 255 | sibbling_id: ActorID, 256 | ) -> Result<(), RemoteSendError> 257 | where 258 | A: Actor, 259 | { 260 | let actor_ref = { 261 | let remote_actors = REMOTE_REGISTRY.lock().await; 262 | remote_actors 263 | .get(&actor_id) 264 | .ok_or(RemoteSendError::ActorNotRunning)? 265 | .actor_ref 266 | .downcast_ref::>() 267 | .ok_or(RemoteSendError::BadActorType)? 268 | .clone() 269 | }; 270 | 271 | actor_ref.links.lock().await.remove(&sibbling_id); 272 | 273 | Ok(()) 274 | } 275 | 276 | pub async fn signal_link_died( 277 | dead_actor_id: ActorID, 278 | notified_actor_id: ActorID, 279 | stop_reason: ActorStopReason, 280 | ) -> Result<(), RemoteSendError> 281 | where 282 | A: Actor, 283 | { 284 | let actor_ref = { 285 | let remote_actors = REMOTE_REGISTRY.lock().await; 286 | remote_actors 287 | .get(¬ified_actor_id) 288 | .ok_or(RemoteSendError::ActorNotRunning)? 289 | .actor_ref 290 | .downcast_ref::>() 291 | .ok_or(RemoteSendError::BadActorType)? 292 | .clone() 293 | }; 294 | 295 | actor_ref 296 | .weak_signal_mailbox() 297 | .signal_link_died(dead_actor_id, stop_reason) 298 | .await?; 299 | 300 | Ok(()) 301 | } 302 | -------------------------------------------------------------------------------- /src/request.rs: -------------------------------------------------------------------------------- 1 | //! Types for sending requests including messages and queries to actors. 2 | 3 | use std::time::Duration; 4 | 5 | mod ask; 6 | mod tell; 7 | 8 | #[cfg(feature = "remote")] 9 | pub use ask::RemoteAskRequest; 10 | 11 | #[cfg(feature = "remote")] 12 | pub use tell::RemoteTellRequest; 13 | 14 | pub use ask::{AskRequest, BlockingPendingReply, PendingReply}; 15 | pub use tell::{RecipientTellRequest, TellRequest}; 16 | 17 | /// A type for requests without any timeout set. 18 | #[derive(Clone, Copy, Debug, Default)] 19 | pub struct WithoutRequestTimeout; 20 | 21 | /// A type for timeouts in actor requests. 22 | #[derive(Clone, Copy, Debug, Default)] 23 | pub struct WithRequestTimeout(Option); 24 | 25 | /// A type which might contain a request timeout. 26 | /// 27 | /// This type is used internally for remote messaging and will panic if used incorrectly with any MessageSend trait. 28 | #[derive(Clone, Copy, Debug)] 29 | pub enum MaybeRequestTimeout { 30 | /// No timeout set. 31 | NoTimeout, 32 | /// A timeout with a duration. 33 | Timeout(Duration), 34 | } 35 | 36 | impl From> for MaybeRequestTimeout { 37 | fn from(timeout: Option) -> Self { 38 | match timeout { 39 | Some(timeout) => MaybeRequestTimeout::Timeout(timeout), 40 | None => MaybeRequestTimeout::NoTimeout, 41 | } 42 | } 43 | } 44 | 45 | impl From for MaybeRequestTimeout { 46 | fn from(_: WithoutRequestTimeout) -> Self { 47 | MaybeRequestTimeout::NoTimeout 48 | } 49 | } 50 | 51 | impl From for MaybeRequestTimeout { 52 | fn from(WithRequestTimeout(timeout): WithRequestTimeout) -> Self { 53 | match timeout { 54 | Some(timeout) => MaybeRequestTimeout::Timeout(timeout), 55 | None => MaybeRequestTimeout::NoTimeout, 56 | } 57 | } 58 | } 59 | 60 | impl From for Option { 61 | fn from(_: WithoutRequestTimeout) -> Self { 62 | None 63 | } 64 | } 65 | 66 | impl From for Option { 67 | fn from(WithRequestTimeout(duration): WithRequestTimeout) -> Self { 68 | duration 69 | } 70 | } 71 | 72 | impl From for Option { 73 | fn from(timeout: MaybeRequestTimeout) -> Self { 74 | match timeout { 75 | MaybeRequestTimeout::NoTimeout => None, 76 | MaybeRequestTimeout::Timeout(duration) => Some(duration), 77 | } 78 | } 79 | } 80 | --------------------------------------------------------------------------------