feat: add NanoID actor refs and REPL bindings
Require explicit behavior selection on spawn, expose randomized actor references, and add named REPL bindings, completion, history navigation, and protocol v2 routing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
d96d603286
commit
4f6e7ccfa2
|
|
@ -46,6 +46,10 @@ All notable changes to this project will be documented in this file.
|
|||
- Execution-model decision and Phase 1 runtime documentation.
|
||||
- Lifecycle tests proving failed-message retry from committed state and
|
||||
stop/restart preservation of reserved-first FIFO order.
|
||||
- NanoID-based public actor references, explicit behavior-and-state spawning,
|
||||
named REPL actor bindings, tab completion, and command history navigation.
|
||||
- NanoID-based public actor references, explicit behavior-and-state spawning,
|
||||
named REPL actor bindings, tab completion, and command history navigation.
|
||||
|
||||
- Runnable Phase 0 experiment pinned to Rune 0.14.2, covering script execution,
|
||||
native calls, async suspension, instruction budgets, frame inspection,
|
||||
|
|
@ -68,6 +72,10 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
- Interactive CLI commands now use the versioned protocol dispatcher instead
|
||||
of calling the image API directly.
|
||||
- Protocol version 2 replaces numeric actor IDs with NanoID actor references
|
||||
and adds optional target references for message effects.
|
||||
- Protocol version 2 replaces numeric actor IDs with NanoID actor references
|
||||
and adds optional target references for message effects.
|
||||
- Actor implementation IDs now identify compiled source/configuration content
|
||||
rather than using sequential process-local numbers.
|
||||
- Expanded the architecture specification with crate ownership and dependency
|
||||
|
|
|
|||
|
|
@ -41,6 +41,12 @@ version = "1.5.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
|
||||
|
||||
[[package]]
|
||||
name = "blake3"
|
||||
version = "1.8.7"
|
||||
|
|
@ -70,6 +76,21 @@ version = "1.0.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
|
||||
|
||||
[[package]]
|
||||
name = "clipboard-win"
|
||||
version = "5.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "052af7878466feccda12ba7a6a67d019d712f296809559b47c12c8cc196d3a9a"
|
||||
dependencies = [
|
||||
"error-code",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codespan-reporting"
|
||||
version = "0.11.1"
|
||||
|
|
@ -77,7 +98,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
|
||||
dependencies = [
|
||||
"termcolor",
|
||||
"unicode-width",
|
||||
"unicode-width 0.1.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -101,6 +122,39 @@ version = "1.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
|
||||
|
||||
[[package]]
|
||||
name = "endian-type"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "error-code"
|
||||
version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7"
|
||||
|
||||
[[package]]
|
||||
name = "fd-lock"
|
||||
version = "4.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"rustix",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.15"
|
||||
|
|
@ -146,6 +200,26 @@ dependencies = [
|
|||
"windows-result",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "home"
|
||||
version = "0.5.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
|
|
@ -164,6 +238,12 @@ version = "0.2.191"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.35"
|
||||
|
|
@ -230,13 +310,43 @@ dependencies = [
|
|||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nanoid"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8"
|
||||
dependencies = [
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nibble_vec"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43"
|
||||
dependencies = [
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -353,6 +463,15 @@ version = "1.15.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
|
|
@ -371,6 +490,46 @@ dependencies = [
|
|||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "radix_trie"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd"
|
||||
dependencies = [
|
||||
"endian-type",
|
||||
"nibble_vec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.18"
|
||||
|
|
@ -394,12 +553,14 @@ version = "0.0.0"
|
|||
dependencies = [
|
||||
"runa-protocol",
|
||||
"runa-server",
|
||||
"rustyline",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "runa-actor"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"nanoid",
|
||||
"runa-core",
|
||||
]
|
||||
|
||||
|
|
@ -566,12 +727,47 @@ dependencies = [
|
|||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5e76742e5294153d4dde1207dbec5bdd81eef6fabdc0d2118babadabfd858aa"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||
|
||||
[[package]]
|
||||
name = "rustyline"
|
||||
version = "15.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ee1e066dc922e513bda599c6ccb5f3bb2b0ea5870a579448f2622993f0a9a2f"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"clipboard-win",
|
||||
"fd-lock",
|
||||
"home",
|
||||
"libc",
|
||||
"log",
|
||||
"memchr",
|
||||
"nix",
|
||||
"radix_trie",
|
||||
"unicode-segmentation",
|
||||
"unicode-width 0.2.3",
|
||||
"utf8parse",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.23"
|
||||
|
|
@ -767,12 +963,30 @@ version = "1.0.27"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca718471876992059f251e33aafc334346ada08042b7661dd1fe313a9bced557"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
|
|
@ -785,13 +999,19 @@ version = "0.9.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -809,6 +1029,15 @@ dependencies = [
|
|||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
|
|
@ -818,6 +1047,70 @@ dependencies = [
|
|||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.63"
|
||||
|
|
|
|||
36
README.md
36
README.md
|
|
@ -67,29 +67,33 @@ intended to connect over the network to this same live image; actors remain in
|
|||
the image regardless of how clients connect. Remote network access requires
|
||||
authentication and authorization, which are not implemented.
|
||||
|
||||
The CLI opens an interactive runtime session using the default counter behavior:
|
||||
The CLI opens an interactive runtime session with the `counter` behavior
|
||||
published:
|
||||
|
||||
```text
|
||||
runa> spawn 0
|
||||
spawned ActorId(1)
|
||||
runa> spawn 10
|
||||
spawned ActorId(2)
|
||||
runa> send 1 1 2
|
||||
runa> let sender = spawn counter 0
|
||||
sender = @<actor-reference>
|
||||
runa> let receiver = spawn counter 10
|
||||
receiver = @<actor-reference>
|
||||
runa> send sender 1 5 --to receiver
|
||||
message queued
|
||||
runa> tick 100
|
||||
runa> inspect 1
|
||||
runa> inspect 2
|
||||
runa> inspect sender
|
||||
runa> inspect receiver
|
||||
runa> events
|
||||
```
|
||||
|
||||
The sender commits state `1` and sends a message to actor 2, which commits
|
||||
state `15`. The CLI also supports publishing Rune source files as named,
|
||||
versioned behaviors, viewing history, rolling back a behavior reference,
|
||||
installing the current version on an idle actor, and cooperatively pausing and
|
||||
resuming or cancelling actor invocations between scheduler ticks. Runtime
|
||||
users can also supply an explicit state migration closure during installation;
|
||||
the CLI's install command preserves state as-is. Use `help` in the session for
|
||||
the complete command list.
|
||||
Actor references are generated NanoIDs rather than sequential numbers. The
|
||||
`let` form binds each spawned actor to a local name; references can also be
|
||||
entered directly with an `@` prefix. The sender commits state `1` and sends a
|
||||
message to the receiver, which commits state `15`. The REPL provides Tab
|
||||
completion and Up/Down command history. It also supports publishing Rune source
|
||||
files as named, versioned behaviors, viewing history, rolling back a behavior
|
||||
reference, installing the current version on an idle actor, and cooperatively
|
||||
pausing and resuming or cancelling actor invocations between scheduler ticks.
|
||||
Runtime users can also supply an explicit state migration closure during
|
||||
installation; the CLI's install command preserves state as-is. Use `help` in
|
||||
the session for the complete command list.
|
||||
See [the feasibility report](docs/rune-feasibility.md) for verified Rune
|
||||
capabilities and limitations, and [the runtime guide](docs/runtime.md) for
|
||||
the runtime contract.
|
||||
|
|
|
|||
|
|
@ -4,4 +4,5 @@ version = "0.0.0"
|
|||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
nanoid = "0.4"
|
||||
runa-core = { path = "../runa-core" }
|
||||
|
|
|
|||
|
|
@ -1,9 +1,72 @@
|
|||
//! Value contracts for the single-worker actor prototype.
|
||||
|
||||
use runa_core::ImplementationId;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct ActorId(pub u64);
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ActorRef([u8; 21]);
|
||||
|
||||
impl ActorRef {
|
||||
pub fn new() -> Self {
|
||||
Self::parse(&nanoid::nanoid!()).expect("Nano ID uses the supported alphabet")
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
std::str::from_utf8(&self.0).expect("Nano ID is valid UTF-8")
|
||||
}
|
||||
|
||||
pub fn parse(value: &str) -> Option<Self> {
|
||||
if value.len() != 21
|
||||
|| !value
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let mut bytes = [0; 21];
|
||||
bytes.copy_from_slice(value.as_bytes());
|
||||
Some(Self(bytes))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ActorRef {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for ActorRef {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(formatter, "@{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ActorRef {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ActorRef;
|
||||
|
||||
#[test]
|
||||
fn generated_actor_references_are_parseable_and_display_without_prefix() {
|
||||
let actor_ref = ActorRef::new();
|
||||
let parsed = ActorRef::parse(actor_ref.as_str()).expect("generated reference is valid");
|
||||
|
||||
assert_eq!(parsed, actor_ref);
|
||||
assert_eq!(actor_ref.to_string().len(), 21);
|
||||
assert_eq!(format!("{actor_ref:?}"), format!("@{actor_ref}"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parsing_rejects_references_outside_the_url_safe_nanoid_shape() {
|
||||
assert!(ActorRef::parse("too-short").is_none());
|
||||
assert!(ActorRef::parse("abcdefghijklmnopqrstu!").is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct MailboxId(pub u64);
|
||||
|
|
@ -12,6 +75,7 @@ pub struct MailboxId(pub u64);
|
|||
pub struct Message {
|
||||
pub kind: i64,
|
||||
pub value: i64,
|
||||
pub target_ref: Option<ActorRef>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
|
|
@ -26,7 +90,7 @@ pub enum Status {
|
|||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ActorSnapshot {
|
||||
pub id: ActorId,
|
||||
pub actor_ref: ActorRef,
|
||||
pub mailbox: MailboxId,
|
||||
pub implementation: ImplementationId,
|
||||
pub incarnation: u64,
|
||||
|
|
@ -46,14 +110,14 @@ pub struct ExecutionSnapshot {
|
|||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Event {
|
||||
Spawned(ActorId),
|
||||
Committed(ActorId),
|
||||
Failed(ActorId, String),
|
||||
Stopped(ActorId),
|
||||
Restarted(ActorId),
|
||||
Paused(ActorId),
|
||||
Resumed(ActorId),
|
||||
InvocationCancelled(ActorId),
|
||||
BehaviorInstalled(ActorId, ImplementationId),
|
||||
StateMigrated(ActorId, i64, i64),
|
||||
Spawned(ActorRef),
|
||||
Committed(ActorRef),
|
||||
Failed(ActorRef, String),
|
||||
Stopped(ActorRef),
|
||||
Restarted(ActorRef),
|
||||
Paused(ActorRef),
|
||||
Resumed(ActorRef),
|
||||
InvocationCancelled(ActorRef),
|
||||
BehaviorInstalled(ActorRef, ImplementationId),
|
||||
StateMigrated(ActorRef, i64, i64),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
use std::num::NonZeroUsize;
|
||||
use std::sync::Arc;
|
||||
|
||||
use runa_actor::{ActorId, ActorSnapshot, Event, Message};
|
||||
use runa_actor::{ActorRef, ActorSnapshot, Event, Message};
|
||||
use runa_code::{BehaviorSnapshot, CodeStore};
|
||||
use runa_core::ImplementationId;
|
||||
use runa_runtime::{Runtime, RuntimeResult};
|
||||
|
|
@ -74,7 +74,7 @@ impl ImageApi {
|
|||
behavior_name: &str,
|
||||
state: i64,
|
||||
capacity: NonZeroUsize,
|
||||
) -> RuntimeResult<ActorId> {
|
||||
) -> RuntimeResult<ActorRef> {
|
||||
let behavior = self
|
||||
.code
|
||||
.behavior(behavior_name)
|
||||
|
|
@ -82,7 +82,7 @@ impl ImageApi {
|
|||
self.runtime.spawn(behavior.current, state, capacity)
|
||||
}
|
||||
|
||||
pub fn send(&mut self, actor: ActorId, message: Message) -> RuntimeResult<()> {
|
||||
pub fn send(&mut self, actor: ActorRef, message: Message) -> RuntimeResult<()> {
|
||||
self.runtime.send(actor, message)
|
||||
}
|
||||
|
||||
|
|
@ -92,7 +92,7 @@ impl ImageApi {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn inspect(&self, actor: ActorId) -> RuntimeResult<ActorSnapshot> {
|
||||
pub fn inspect(&self, actor: ActorRef) -> RuntimeResult<ActorSnapshot> {
|
||||
self.runtime.inspect(actor)
|
||||
}
|
||||
|
||||
|
|
@ -104,29 +104,29 @@ impl ImageApi {
|
|||
self.runtime.drain_events()
|
||||
}
|
||||
|
||||
pub fn stop(&mut self, actor: ActorId) -> RuntimeResult<()> {
|
||||
pub fn stop(&mut self, actor: ActorRef) -> RuntimeResult<()> {
|
||||
self.runtime.stop(actor)
|
||||
}
|
||||
|
||||
pub fn restart(&mut self, actor: ActorId) -> RuntimeResult<()> {
|
||||
pub fn restart(&mut self, actor: ActorRef) -> RuntimeResult<()> {
|
||||
self.runtime.restart(actor)
|
||||
}
|
||||
|
||||
pub fn pause(&mut self, actor: ActorId) -> RuntimeResult<()> {
|
||||
pub fn pause(&mut self, actor: ActorRef) -> RuntimeResult<()> {
|
||||
self.runtime.pause(actor)
|
||||
}
|
||||
|
||||
pub fn resume(&mut self, actor: ActorId) -> RuntimeResult<()> {
|
||||
pub fn resume(&mut self, actor: ActorRef) -> RuntimeResult<()> {
|
||||
self.runtime.resume(actor)
|
||||
}
|
||||
|
||||
pub fn cancel(&mut self, actor: ActorId) -> RuntimeResult<()> {
|
||||
pub fn cancel(&mut self, actor: ActorRef) -> RuntimeResult<()> {
|
||||
self.runtime.cancel(actor)
|
||||
}
|
||||
|
||||
pub fn install(
|
||||
&mut self,
|
||||
actor: ActorId,
|
||||
actor: ActorRef,
|
||||
behavior_name: &str,
|
||||
) -> RuntimeResult<ImplementationId> {
|
||||
self.install_with_migration(actor, behavior_name, Ok)
|
||||
|
|
@ -134,7 +134,7 @@ impl ImageApi {
|
|||
|
||||
pub fn install_with_migration(
|
||||
&mut self,
|
||||
actor: ActorId,
|
||||
actor: ActorRef,
|
||||
behavior_name: &str,
|
||||
migrate: impl FnOnce(i64) -> RuntimeResult<i64>,
|
||||
) -> RuntimeResult<ImplementationId> {
|
||||
|
|
@ -162,14 +162,21 @@ mod tests {
|
|||
let behavior = image.publish_behavior(
|
||||
"counter",
|
||||
None,
|
||||
"pub fn handle(state, kind, value) { (state + value, []) }",
|
||||
"pub fn handle(state, kind, value, target_ref) { (state + value, []) }",
|
||||
)?;
|
||||
assert_eq!(
|
||||
image.behavior_source("counter", behavior.revision)?,
|
||||
"pub fn handle(state, kind, value) { (state + value, []) }"
|
||||
"pub fn handle(state, kind, value, target_ref) { (state + value, []) }"
|
||||
);
|
||||
let actor = image.spawn("counter", 5, capacity(2))?;
|
||||
image.send(actor, Message { kind: 0, value: 7 })?;
|
||||
image.send(
|
||||
actor,
|
||||
Message {
|
||||
kind: 0,
|
||||
value: 7,
|
||||
target_ref: None,
|
||||
},
|
||||
)?;
|
||||
image.tick(100, capacity(10));
|
||||
|
||||
assert_eq!(image.inspect(actor)?.state, 12);
|
||||
|
|
@ -178,7 +185,7 @@ mod tests {
|
|||
image
|
||||
.events()
|
||||
.iter()
|
||||
.any(|event| matches!(event, Event::Committed(id) if *id == actor))
|
||||
.any(|event| matches!(event, Event::Committed(id) if id == &actor))
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -189,14 +196,14 @@ mod tests {
|
|||
let first = image.publish_behavior(
|
||||
"counter",
|
||||
None,
|
||||
"pub fn handle(state, kind, value) { (state + value, []) }",
|
||||
"pub fn handle(state, kind, value, target_ref) { (state + value, []) }",
|
||||
)?;
|
||||
assert!(
|
||||
image
|
||||
.publish_behavior(
|
||||
"counter",
|
||||
None,
|
||||
"pub fn handle(state, kind, value) { (state - value, []) }"
|
||||
"pub fn handle(state, kind, value, target_ref) { (state - value, []) }"
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
|
|
@ -210,12 +217,12 @@ mod tests {
|
|||
let first = image.publish_behavior(
|
||||
"counter",
|
||||
None,
|
||||
"pub fn handle(state, kind, value) { (state + value, []) }",
|
||||
"pub fn handle(state, kind, value, target_ref) { (state + value, []) }",
|
||||
)?;
|
||||
image.publish_behavior(
|
||||
"counter",
|
||||
Some(first.revision),
|
||||
"pub fn handle(state, kind, value) { (state * value, []) }",
|
||||
"pub fn handle(state, kind, value, target_ref) { (state * value, []) }",
|
||||
)?;
|
||||
let actor = image.spawn("counter", 5, capacity(2))?;
|
||||
let implementation = image.install_with_migration(actor, "counter", |state| {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const PROTOCOL_VERSION: u16 = 1;
|
||||
pub const PROTOCOL_VERSION: u16 = 2;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
|
|
@ -60,18 +60,23 @@ impl Request {
|
|||
return Err(invalid_request("mailbox_capacity must be positive"));
|
||||
}
|
||||
}
|
||||
Command::Send { actor_id, .. }
|
||||
| Command::InspectActor { actor_id }
|
||||
| Command::StopActor { actor_id }
|
||||
| Command::RestartActor { actor_id }
|
||||
| Command::PauseActor { actor_id }
|
||||
| Command::ResumeActor { actor_id }
|
||||
| Command::CancelActor { actor_id } => validate_actor_id(*actor_id)?,
|
||||
Command::Send { actor_ref, message } => {
|
||||
validate_actor_ref(actor_ref)?;
|
||||
if let Some(target_ref) = &message.target_ref {
|
||||
validate_actor_ref(target_ref)?;
|
||||
}
|
||||
}
|
||||
Command::InspectActor { actor_ref }
|
||||
| Command::StopActor { actor_ref }
|
||||
| Command::RestartActor { actor_ref }
|
||||
| Command::PauseActor { actor_ref }
|
||||
| Command::ResumeActor { actor_ref }
|
||||
| Command::CancelActor { actor_ref } => validate_actor_ref(actor_ref)?,
|
||||
Command::InstallBehavior {
|
||||
actor_id,
|
||||
actor_ref,
|
||||
behavior_name,
|
||||
} => {
|
||||
validate_actor_id(*actor_id)?;
|
||||
validate_actor_ref(actor_ref)?;
|
||||
validate_name(behavior_name)?;
|
||||
}
|
||||
Command::Tick {
|
||||
|
|
@ -115,7 +120,7 @@ pub enum Command {
|
|||
mailbox_capacity: usize,
|
||||
},
|
||||
Send {
|
||||
actor_id: u64,
|
||||
actor_ref: String,
|
||||
message: MessageDto,
|
||||
},
|
||||
Tick {
|
||||
|
|
@ -123,27 +128,27 @@ pub enum Command {
|
|||
instruction_budget: usize,
|
||||
},
|
||||
InspectActor {
|
||||
actor_id: u64,
|
||||
actor_ref: String,
|
||||
},
|
||||
ListActors,
|
||||
GetEvents,
|
||||
StopActor {
|
||||
actor_id: u64,
|
||||
actor_ref: String,
|
||||
},
|
||||
RestartActor {
|
||||
actor_id: u64,
|
||||
actor_ref: String,
|
||||
},
|
||||
PauseActor {
|
||||
actor_id: u64,
|
||||
actor_ref: String,
|
||||
},
|
||||
ResumeActor {
|
||||
actor_id: u64,
|
||||
actor_ref: String,
|
||||
},
|
||||
CancelActor {
|
||||
actor_id: u64,
|
||||
actor_ref: String,
|
||||
},
|
||||
InstallBehavior {
|
||||
actor_id: u64,
|
||||
actor_ref: String,
|
||||
behavior_name: String,
|
||||
},
|
||||
}
|
||||
|
|
@ -163,7 +168,7 @@ pub enum Reply {
|
|||
Behavior(BehaviorDto),
|
||||
Behaviors(Vec<BehaviorDto>),
|
||||
BehaviorSource { source: String },
|
||||
ActorSpawned { actor_id: u64 },
|
||||
ActorSpawned { actor_ref: String },
|
||||
Actor(ActorSnapshotDto),
|
||||
Actors(Vec<ActorSnapshotDto>),
|
||||
Events(Vec<EventDto>),
|
||||
|
|
@ -182,36 +187,36 @@ pub struct ProtocolEvent {
|
|||
#[serde(tag = "event", content = "data", rename_all = "snake_case")]
|
||||
pub enum EventDto {
|
||||
Spawned {
|
||||
actor_id: u64,
|
||||
actor_ref: String,
|
||||
},
|
||||
Committed {
|
||||
actor_id: u64,
|
||||
actor_ref: String,
|
||||
},
|
||||
Failed {
|
||||
actor_id: u64,
|
||||
actor_ref: String,
|
||||
message: String,
|
||||
},
|
||||
Stopped {
|
||||
actor_id: u64,
|
||||
actor_ref: String,
|
||||
},
|
||||
Restarted {
|
||||
actor_id: u64,
|
||||
actor_ref: String,
|
||||
},
|
||||
Paused {
|
||||
actor_id: u64,
|
||||
actor_ref: String,
|
||||
},
|
||||
Resumed {
|
||||
actor_id: u64,
|
||||
actor_ref: String,
|
||||
},
|
||||
InvocationCancelled {
|
||||
actor_id: u64,
|
||||
actor_ref: String,
|
||||
},
|
||||
BehaviorInstalled {
|
||||
actor_id: u64,
|
||||
actor_ref: String,
|
||||
implementation_id: String,
|
||||
},
|
||||
StateMigrated {
|
||||
actor_id: u64,
|
||||
actor_ref: String,
|
||||
previous_state: i64,
|
||||
current_state: i64,
|
||||
},
|
||||
|
|
@ -233,17 +238,19 @@ pub struct BehaviorVersionDto {
|
|||
pub implementation_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct MessageDto {
|
||||
pub kind: i64,
|
||||
pub value: i64,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub target_ref: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ActorSnapshotDto {
|
||||
pub actor_id: u64,
|
||||
pub actor_ref: String,
|
||||
pub mailbox_id: u64,
|
||||
pub implementation_id: String,
|
||||
pub incarnation: u64,
|
||||
|
|
@ -307,9 +314,15 @@ fn validate_name(name: &str) -> Result<(), ProtocolError> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_actor_id(actor_id: u64) -> Result<(), ProtocolError> {
|
||||
if actor_id == 0 {
|
||||
return Err(invalid_request("actor_id must be positive"));
|
||||
fn validate_actor_ref(actor_ref: &str) -> Result<(), ProtocolError> {
|
||||
if actor_ref.len() != 21
|
||||
|| !actor_ref
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
|
||||
{
|
||||
return Err(invalid_request(
|
||||
"actor_ref must be a 21-character URL-safe identifier",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -347,8 +360,12 @@ mod tests {
|
|||
#[test]
|
||||
fn request_uses_a_stable_tagged_wire_shape() {
|
||||
let value = serde_json::to_value(request(Command::Send {
|
||||
actor_id: 7,
|
||||
message: MessageDto { kind: 2, value: 9 },
|
||||
actor_ref: "xK8_pQ2-Lm5_012345678".to_owned(),
|
||||
message: MessageDto {
|
||||
kind: 2,
|
||||
value: 9,
|
||||
target_ref: None,
|
||||
},
|
||||
}))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
|
|
@ -359,7 +376,7 @@ mod tests {
|
|||
"command": {
|
||||
"operation": "send",
|
||||
"parameters": {
|
||||
"actor_id": 7,
|
||||
"actor_ref": "xK8_pQ2-Lm5_012345678",
|
||||
"message": { "kind": 2, "value": 9 }
|
||||
}
|
||||
}
|
||||
|
|
@ -395,7 +412,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn requests_reject_empty_correlation_ids_and_zero_actor_ids() {
|
||||
fn requests_reject_empty_correlation_ids_and_malformed_actor_refs() {
|
||||
let mut request = request(Command::GetEvents);
|
||||
request.request_id = " ".to_owned();
|
||||
assert_eq!(
|
||||
|
|
@ -404,7 +421,35 @@ mod tests {
|
|||
);
|
||||
|
||||
request.request_id = "req-2".to_owned();
|
||||
request.command = Command::StopActor { actor_id: 0 };
|
||||
request.command = Command::StopActor {
|
||||
actor_ref: String::new(),
|
||||
};
|
||||
assert_eq!(
|
||||
request.validate().unwrap_err().code,
|
||||
ErrorCode::InvalidRequest
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_target_reference_is_optional_and_validated_when_present() {
|
||||
let mut request = request(Command::Send {
|
||||
actor_ref: "xK8_pQ2-Lm5_012345678".to_owned(),
|
||||
message: MessageDto {
|
||||
kind: 2,
|
||||
value: 9,
|
||||
target_ref: Some("Hj2_9sM0-pQ4_7aBcD3eF".to_owned()),
|
||||
},
|
||||
});
|
||||
assert_eq!(request.validate(), Ok(()));
|
||||
|
||||
request.command = Command::Send {
|
||||
actor_ref: "xK8_pQ2-Lm5_012345678".to_owned(),
|
||||
message: MessageDto {
|
||||
kind: 2,
|
||||
value: 9,
|
||||
target_ref: Some("bad".to_owned()),
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
request.validate().unwrap_err().code,
|
||||
ErrorCode::InvalidRequest
|
||||
|
|
@ -428,7 +473,7 @@ mod tests {
|
|||
let event = ProtocolEvent {
|
||||
version: PROTOCOL_VERSION,
|
||||
event: EventDto::StateMigrated {
|
||||
actor_id: 3,
|
||||
actor_ref: "actor_ref_000000000000".to_owned(),
|
||||
previous_state: 4,
|
||||
current_state: 40,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@ use std::sync::Arc;
|
|||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::task::{Context, Wake, Waker};
|
||||
|
||||
use runa_actor::{ActorId, ActorSnapshot, Event, ExecutionSnapshot, MailboxId, Message, Status};
|
||||
use runa_actor::{ActorRef, ActorSnapshot, Event, ExecutionSnapshot, MailboxId, Message, Status};
|
||||
use runa_code::CompiledArtifact;
|
||||
use runa_core::ImplementationId;
|
||||
use runa_rune::{ExperimentResult, Invocation, Slice};
|
||||
|
||||
type WireTransition = (i64, Vec<(u64, i64, i64)>);
|
||||
type WireTransition = (i64, Vec<(String, i64, i64)>);
|
||||
pub type RuntimeResult<T> = ExperimentResult<T>;
|
||||
|
||||
struct WakeFlag(AtomicBool);
|
||||
|
|
@ -36,9 +36,9 @@ struct Actor {
|
|||
|
||||
#[derive(Default)]
|
||||
pub struct Runtime {
|
||||
actors: BTreeMap<ActorId, Actor>,
|
||||
actors: BTreeMap<ActorRef, Actor>,
|
||||
implementations: BTreeMap<ImplementationId, Arc<CompiledArtifact>>,
|
||||
next_actor: u64,
|
||||
next_mailbox: u64,
|
||||
events: Vec<Event>,
|
||||
}
|
||||
|
||||
|
|
@ -63,21 +63,24 @@ impl Runtime {
|
|||
implementation: ImplementationId,
|
||||
state: i64,
|
||||
capacity: NonZeroUsize,
|
||||
) -> ExperimentResult<ActorId> {
|
||||
) -> ExperimentResult<ActorRef> {
|
||||
if !self.implementations.contains_key(&implementation) {
|
||||
return Err("unknown implementation".into());
|
||||
}
|
||||
self.next_actor = self
|
||||
.next_actor
|
||||
self.next_mailbox = self
|
||||
.next_mailbox
|
||||
.checked_add(1)
|
||||
.ok_or("actor IDs exhausted")?;
|
||||
let id = ActorId(self.next_actor);
|
||||
.ok_or("mailbox IDs exhausted")?;
|
||||
let mut actor_ref = ActorRef::new();
|
||||
while self.actors.contains_key(&actor_ref) {
|
||||
actor_ref = ActorRef::new();
|
||||
}
|
||||
self.actors.insert(
|
||||
id,
|
||||
actor_ref,
|
||||
Actor {
|
||||
snapshot: ActorSnapshot {
|
||||
id,
|
||||
mailbox: MailboxId(id.0),
|
||||
actor_ref,
|
||||
mailbox: MailboxId(self.next_mailbox),
|
||||
implementation,
|
||||
incarnation: 1,
|
||||
state,
|
||||
|
|
@ -93,11 +96,11 @@ impl Runtime {
|
|||
paused_from: None,
|
||||
},
|
||||
);
|
||||
self.events.push(Event::Spawned(id));
|
||||
Ok(id)
|
||||
self.events.push(Event::Spawned(actor_ref));
|
||||
Ok(actor_ref)
|
||||
}
|
||||
|
||||
pub fn send(&mut self, id: ActorId, message: Message) -> ExperimentResult<()> {
|
||||
pub fn send(&mut self, id: ActorRef, message: Message) -> ExperimentResult<()> {
|
||||
let actor = self.actors.get_mut(&id).ok_or("unknown actor")?;
|
||||
if matches!(actor.snapshot.status, Status::Stopped | Status::Failed(_)) {
|
||||
return Err("actor is not accepting messages".into());
|
||||
|
|
@ -111,7 +114,7 @@ impl Runtime {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn inspect(&self, id: ActorId) -> ExperimentResult<ActorSnapshot> {
|
||||
pub fn inspect(&self, id: ActorRef) -> ExperimentResult<ActorSnapshot> {
|
||||
let actor = self.actors.get(&id).ok_or("unknown actor")?;
|
||||
let mut snapshot = actor.snapshot.clone();
|
||||
snapshot.execution = actor.active.as_ref().map(|invocation| {
|
||||
|
|
@ -145,7 +148,7 @@ impl Runtime {
|
|||
.collect()
|
||||
}
|
||||
|
||||
pub fn stop(&mut self, id: ActorId) -> ExperimentResult<()> {
|
||||
pub fn stop(&mut self, id: ActorRef) -> ExperimentResult<()> {
|
||||
let actor = self.actors.get_mut(&id).ok_or("unknown actor")?;
|
||||
actor.active = None;
|
||||
actor.paused_from = None;
|
||||
|
|
@ -155,7 +158,7 @@ impl Runtime {
|
|||
}
|
||||
|
||||
/// Pause scheduling between worker ticks without discarding an invocation.
|
||||
pub fn pause(&mut self, id: ActorId) -> ExperimentResult<()> {
|
||||
pub fn pause(&mut self, id: ActorRef) -> ExperimentResult<()> {
|
||||
let actor = self.actors.get_mut(&id).ok_or("unknown actor")?;
|
||||
if matches!(
|
||||
actor.snapshot.status,
|
||||
|
|
@ -170,7 +173,7 @@ impl Runtime {
|
|||
}
|
||||
|
||||
/// Resume from the exact runnable or awaiting state saved by `pause`.
|
||||
pub fn resume(&mut self, id: ActorId) -> ExperimentResult<()> {
|
||||
pub fn resume(&mut self, id: ActorRef) -> ExperimentResult<()> {
|
||||
let actor = self.actors.get_mut(&id).ok_or("unknown actor")?;
|
||||
let status = actor.paused_from.take().ok_or("actor is not paused")?;
|
||||
actor.snapshot.status = status;
|
||||
|
|
@ -181,7 +184,7 @@ impl Runtime {
|
|||
/// Cancel an active handler and requeue its reserved message for retry.
|
||||
///
|
||||
/// This does not roll back side effects already performed by native code.
|
||||
pub fn cancel(&mut self, id: ActorId) -> ExperimentResult<()> {
|
||||
pub fn cancel(&mut self, id: ActorRef) -> ExperimentResult<()> {
|
||||
let actor = self.actors.get_mut(&id).ok_or("unknown actor")?;
|
||||
if actor.active.is_none() {
|
||||
return Err("actor has no active invocation to cancel".into());
|
||||
|
|
@ -199,7 +202,7 @@ impl Runtime {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn restart(&mut self, id: ActorId) -> ExperimentResult<()> {
|
||||
pub fn restart(&mut self, id: ActorRef) -> ExperimentResult<()> {
|
||||
let actor = self.actors.get_mut(&id).ok_or("unknown actor")?;
|
||||
if !matches!(actor.snapshot.status, Status::Stopped | Status::Failed(_)) {
|
||||
return Err("stop the actor before restarting".into());
|
||||
|
|
@ -221,7 +224,7 @@ impl Runtime {
|
|||
/// Install an immutable implementation between handler invocations.
|
||||
pub fn install(
|
||||
&mut self,
|
||||
id: ActorId,
|
||||
id: ActorRef,
|
||||
implementation: ImplementationId,
|
||||
) -> ExperimentResult<()> {
|
||||
self.install_with_migration(id, implementation, Ok)
|
||||
|
|
@ -231,7 +234,7 @@ impl Runtime {
|
|||
/// operation. A migration error leaves both unchanged.
|
||||
pub fn install_with_migration(
|
||||
&mut self,
|
||||
id: ActorId,
|
||||
id: ActorRef,
|
||||
implementation: ImplementationId,
|
||||
migrate: impl FnOnce(i64) -> ExperimentResult<i64>,
|
||||
) -> ExperimentResult<()> {
|
||||
|
|
@ -265,18 +268,18 @@ impl Runtime {
|
|||
std::mem::take(&mut self.events)
|
||||
}
|
||||
|
||||
fn fail(&mut self, id: ActorId, error: String) {
|
||||
let actor = self.actors.get_mut(&id).expect("scheduled actor exists");
|
||||
fn fail(&mut self, id: &ActorRef, error: String) {
|
||||
let actor = self.actors.get_mut(id).expect("scheduled actor exists");
|
||||
actor.active = None;
|
||||
actor.snapshot.status = Status::Failed(error.clone());
|
||||
self.events.push(Event::Failed(id, error));
|
||||
self.events.push(Event::Failed(*id, error));
|
||||
}
|
||||
|
||||
fn commit(&mut self, id: ActorId, transition: WireTransition) -> ExperimentResult<()> {
|
||||
fn commit(&mut self, id: &ActorRef, transition: WireTransition) -> ExperimentResult<()> {
|
||||
let (state, effects) = transition;
|
||||
let mut counts = BTreeMap::<ActorId, usize>::new();
|
||||
for &(target, _, _) in &effects {
|
||||
let target = ActorId(target);
|
||||
let mut counts = BTreeMap::<ActorRef, usize>::new();
|
||||
for (target, _, _) in &effects {
|
||||
let target = ActorRef::parse(target).ok_or("effect targets invalid actor reference")?;
|
||||
let actor = self
|
||||
.actors
|
||||
.get(&target)
|
||||
|
|
@ -286,24 +289,29 @@ impl Runtime {
|
|||
}
|
||||
let count = counts.entry(target).or_default();
|
||||
*count += 1;
|
||||
let retained = usize::from(target != id && actor.snapshot.reserved.is_some());
|
||||
let retained = usize::from(target != *id && actor.snapshot.reserved.is_some());
|
||||
if actor.mailbox.len() + retained + *count > actor.capacity.get() {
|
||||
return Err("effect exceeds mailbox capacity".into());
|
||||
}
|
||||
}
|
||||
let actor = self.actors.get_mut(&id).expect("scheduled actor exists");
|
||||
let actor = self.actors.get_mut(id).expect("scheduled actor exists");
|
||||
actor.snapshot.state = state;
|
||||
actor.snapshot.reserved = None;
|
||||
actor.snapshot.status = Status::Idle;
|
||||
actor.active = None;
|
||||
for (target, kind, value) in effects {
|
||||
let target = ActorRef::parse(&target).expect("validated target");
|
||||
self.actors
|
||||
.get_mut(&ActorId(target))
|
||||
.get_mut(&target)
|
||||
.expect("validated target")
|
||||
.mailbox
|
||||
.push_back(Message { kind, value });
|
||||
.push_back(Message {
|
||||
kind,
|
||||
value,
|
||||
target_ref: None,
|
||||
});
|
||||
}
|
||||
self.events.push(Event::Committed(id));
|
||||
self.events.push(Event::Committed(*id));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -327,11 +335,18 @@ impl Runtime {
|
|||
match Invocation::start(
|
||||
program.vm(),
|
||||
"handle",
|
||||
(actor.snapshot.state, message.kind, message.value),
|
||||
(
|
||||
actor.snapshot.state,
|
||||
message.kind,
|
||||
message.value,
|
||||
message
|
||||
.target_ref
|
||||
.map_or_else(String::new, |target_ref| target_ref.to_string()),
|
||||
),
|
||||
) {
|
||||
Ok(invocation) => actor.active = Some(invocation),
|
||||
Err(error) => {
|
||||
self.fail(id, error.to_string());
|
||||
self.fail(&id, error.to_string());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
|
@ -350,13 +365,13 @@ impl Runtime {
|
|||
.poll_slice(&mut cx, budget);
|
||||
match outcome {
|
||||
Slice::Completed(transition) => {
|
||||
if let Err(error) = self.commit(id, transition) {
|
||||
self.fail(id, error.to_string());
|
||||
if let Err(error) = self.commit(&id, transition) {
|
||||
self.fail(&id, error.to_string());
|
||||
}
|
||||
}
|
||||
Slice::Awaiting => actor.snapshot.status = Status::Awaiting,
|
||||
Slice::BudgetExhausted => actor.snapshot.status = Status::Running,
|
||||
Slice::Failed(error) => self.fail(id, error.to_string()),
|
||||
Slice::Failed(error) => self.fail(&id, error.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -380,7 +395,7 @@ mod tests {
|
|||
let mut runtime = Runtime::default();
|
||||
let code = publish(
|
||||
&mut runtime,
|
||||
"pub fn handle(state, kind, value) { if kind == 1 { (state + 1, [(value, 0, 5)]) } else { (state + value, []) } }",
|
||||
"pub fn handle(state, kind, value, target_ref) { if kind == 1 { (state + 1, [(target_ref, 0, 5)]) } else { (state + value, []) } }",
|
||||
)?;
|
||||
let first = runtime.spawn(code, 0, capacity(4))?;
|
||||
let second = runtime.spawn(code, 10, capacity(4))?;
|
||||
|
|
@ -388,7 +403,8 @@ mod tests {
|
|||
first,
|
||||
Message {
|
||||
kind: 1,
|
||||
value: i64::try_from(second.0)?,
|
||||
value: 0,
|
||||
target_ref: Some(second),
|
||||
},
|
||||
)?;
|
||||
for _ in 0..100 {
|
||||
|
|
@ -411,23 +427,48 @@ mod tests {
|
|||
let mut runtime = Runtime::default();
|
||||
let runaway = publish(
|
||||
&mut runtime,
|
||||
"pub fn handle(state, kind, value) { loop {} }",
|
||||
"pub fn handle(state, kind, value, target_ref) { loop {} }",
|
||||
)?;
|
||||
let healthy = publish(
|
||||
&mut runtime,
|
||||
"pub fn handle(state, kind, value) { (state + value, []) }",
|
||||
"pub fn handle(state, kind, value, target_ref) { (state + value, []) }",
|
||||
)?;
|
||||
let first = runtime.spawn(runaway, 0, capacity(1))?;
|
||||
let second = runtime.spawn(healthy, 0, capacity(2))?;
|
||||
runtime.send(first, Message { kind: 0, value: 1 })?;
|
||||
runtime.send(second, Message { kind: 0, value: 42 })?;
|
||||
runtime.send(
|
||||
first,
|
||||
Message {
|
||||
kind: 0,
|
||||
value: 1,
|
||||
target_ref: None,
|
||||
},
|
||||
)?;
|
||||
runtime.send(
|
||||
second,
|
||||
Message {
|
||||
kind: 0,
|
||||
value: 42,
|
||||
target_ref: None,
|
||||
},
|
||||
)?;
|
||||
for _ in 0..100 {
|
||||
runtime.tick(capacity(5));
|
||||
}
|
||||
assert_eq!(runtime.inspect(first)?.status, Status::Running);
|
||||
assert_eq!(runtime.inspect(first)?.state, 0);
|
||||
assert_eq!(runtime.inspect(second)?.state, 42);
|
||||
assert!(runtime.send(first, Message { kind: 0, value: 2 }).is_err());
|
||||
assert!(
|
||||
runtime
|
||||
.send(
|
||||
first,
|
||||
Message {
|
||||
kind: 0,
|
||||
value: 2,
|
||||
target_ref: None
|
||||
}
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
runtime.stop(first)?;
|
||||
assert_eq!(runtime.inspect(first)?.status, Status::Stopped);
|
||||
runtime.restart(first)?;
|
||||
|
|
@ -442,10 +483,17 @@ mod tests {
|
|||
let mut runtime = Runtime::default();
|
||||
let implementation = publish(
|
||||
&mut runtime,
|
||||
"pub fn handle(state, kind, value) { loop {} }",
|
||||
"pub fn handle(state, kind, value, target_ref) { loop {} }",
|
||||
)?;
|
||||
let actor = runtime.spawn(implementation, 0, capacity(2))?;
|
||||
runtime.send(actor, Message { kind: 0, value: 1 })?;
|
||||
runtime.send(
|
||||
actor,
|
||||
Message {
|
||||
kind: 0,
|
||||
value: 1,
|
||||
target_ref: None,
|
||||
},
|
||||
)?;
|
||||
runtime.tick(capacity(3));
|
||||
|
||||
let running = runtime.inspect(actor)?;
|
||||
|
|
@ -467,7 +515,14 @@ mod tests {
|
|||
runtime.cancel(actor)?;
|
||||
let cancelled = runtime.inspect(actor)?;
|
||||
assert_eq!(cancelled.execution, None);
|
||||
assert_eq!(cancelled.queued, vec![Message { kind: 0, value: 1 }]);
|
||||
assert_eq!(
|
||||
cancelled.queued,
|
||||
vec![Message {
|
||||
kind: 0,
|
||||
value: 1,
|
||||
target_ref: None
|
||||
}]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -476,14 +531,15 @@ mod tests {
|
|||
let mut runtime = Runtime::default();
|
||||
let code = publish(
|
||||
&mut runtime,
|
||||
"pub fn handle(state, kind, value) { (99, [(value, 0, 1), (999, 0, 2)]) }",
|
||||
"pub fn handle(state, kind, value, target_ref) { (99, [(target_ref, 0, 1), (\"invalid\", 0, 2)]) }",
|
||||
)?;
|
||||
let actor = runtime.spawn(code, 5, capacity(4))?;
|
||||
runtime.send(
|
||||
actor,
|
||||
Message {
|
||||
kind: 0,
|
||||
value: i64::try_from(actor.0)?,
|
||||
value: 0,
|
||||
target_ref: Some(actor),
|
||||
},
|
||||
)?;
|
||||
for _ in 0..100 {
|
||||
|
|
@ -539,14 +595,28 @@ mod tests {
|
|||
let mut runtime = Runtime::default();
|
||||
let code = runtime.publish(
|
||||
CodeStore::default().compile_with_module(
|
||||
"pub async fn handle(state, kind, value) { if kind == 1 { let x = wait().await; (state + x, []) } else { (state + value, []) } }",
|
||||
"pub async fn handle(state, kind, value, target_ref) { if kind == 1 { let x = wait().await; (state + x, []) } else { (state + value, []) } }",
|
||||
"test-gate:v1",
|
||||
module,
|
||||
)?,
|
||||
)?;
|
||||
let actor = runtime.spawn(code, 10, capacity(2))?;
|
||||
runtime.send(actor, Message { kind: 1, value: 0 })?;
|
||||
runtime.send(actor, Message { kind: 0, value: 3 })?;
|
||||
runtime.send(
|
||||
actor,
|
||||
Message {
|
||||
kind: 1,
|
||||
value: 0,
|
||||
target_ref: None,
|
||||
},
|
||||
)?;
|
||||
runtime.send(
|
||||
actor,
|
||||
Message {
|
||||
kind: 0,
|
||||
value: 3,
|
||||
target_ref: None,
|
||||
},
|
||||
)?;
|
||||
for _ in 0..100 {
|
||||
runtime.tick(capacity(5));
|
||||
}
|
||||
|
|
@ -554,7 +624,14 @@ mod tests {
|
|||
assert_eq!(pending.status, Status::Awaiting);
|
||||
assert_eq!(pending.state, 10);
|
||||
assert_eq!(pending.queued.len(), 1);
|
||||
assert_eq!(pending.reserved, Some(Message { kind: 1, value: 0 }));
|
||||
assert_eq!(
|
||||
pending.reserved,
|
||||
Some(Message {
|
||||
kind: 1,
|
||||
value: 0,
|
||||
target_ref: None
|
||||
})
|
||||
);
|
||||
let polls = gate.polls.load(Ordering::SeqCst);
|
||||
for _ in 0..10 {
|
||||
runtime.tick(capacity(5));
|
||||
|
|
@ -586,7 +663,14 @@ mod tests {
|
|||
|
||||
gate.ready.store(false, Ordering::SeqCst);
|
||||
gate.dropped.store(false, Ordering::SeqCst);
|
||||
runtime.send(actor, Message { kind: 1, value: 0 })?;
|
||||
runtime.send(
|
||||
actor,
|
||||
Message {
|
||||
kind: 1,
|
||||
value: 0,
|
||||
target_ref: None,
|
||||
},
|
||||
)?;
|
||||
for _ in 0..100 {
|
||||
runtime.tick(capacity(5));
|
||||
}
|
||||
|
|
@ -597,7 +681,14 @@ mod tests {
|
|||
assert_eq!(cancelled.mailbox, pending.mailbox);
|
||||
assert_eq!(cancelled.state, 20);
|
||||
assert_eq!(cancelled.status, Status::Idle);
|
||||
assert_eq!(cancelled.queued, vec![Message { kind: 1, value: 0 }]);
|
||||
assert_eq!(
|
||||
cancelled.queued,
|
||||
vec![Message {
|
||||
kind: 1,
|
||||
value: 0,
|
||||
target_ref: None
|
||||
}]
|
||||
);
|
||||
assert!(cancelled.reserved.is_none());
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -607,10 +698,17 @@ mod tests {
|
|||
let mut runtime = Runtime::default();
|
||||
let implementation = publish(
|
||||
&mut runtime,
|
||||
"pub fn handle(state, kind, value) { (state + value, []) }",
|
||||
"pub fn handle(state, kind, value, target_ref) { (state + value, []) }",
|
||||
)?;
|
||||
let actor = runtime.spawn(implementation, 10, capacity(2))?;
|
||||
runtime.send(actor, Message { kind: 0, value: 7 })?;
|
||||
runtime.send(
|
||||
actor,
|
||||
Message {
|
||||
kind: 0,
|
||||
value: 7,
|
||||
target_ref: None,
|
||||
},
|
||||
)?;
|
||||
runtime.tick(capacity(1));
|
||||
let before_pause = runtime.inspect(actor)?;
|
||||
assert_eq!(before_pause.status, Status::Running);
|
||||
|
|
@ -655,12 +753,30 @@ mod tests {
|
|||
let mut runtime = Runtime::default();
|
||||
let code = publish(
|
||||
&mut runtime,
|
||||
"pub fn handle(state, kind, value) { (state / value, []) }",
|
||||
"pub fn handle(state, kind, value, target_ref) { (state / value, []) }",
|
||||
)?;
|
||||
let actor = runtime.spawn(code, 5, capacity(1))?;
|
||||
runtime.send(actor, Message { kind: 0, value: 0 })?;
|
||||
assert!(runtime.send(actor, Message { kind: 0, value: 1 }).is_err());
|
||||
assert!(runtime.inspect(ActorId(999)).is_err());
|
||||
runtime.send(
|
||||
actor,
|
||||
Message {
|
||||
kind: 0,
|
||||
value: 0,
|
||||
target_ref: None,
|
||||
},
|
||||
)?;
|
||||
assert!(
|
||||
runtime
|
||||
.send(
|
||||
actor,
|
||||
Message {
|
||||
kind: 0,
|
||||
value: 1,
|
||||
target_ref: None
|
||||
}
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
assert!(runtime.inspect(ActorRef::new()).is_err());
|
||||
for _ in 0..100 {
|
||||
runtime.tick(capacity(5));
|
||||
}
|
||||
|
|
@ -680,14 +796,15 @@ mod tests {
|
|||
let mut runtime = Runtime::default();
|
||||
let code = publish(
|
||||
&mut runtime,
|
||||
"pub fn handle(state, kind, value) { (99, [(value, 0, 1), (value, 0, 2)]) }",
|
||||
"pub fn handle(state, kind, value, target_ref) { (99, [(target_ref, 0, 1), (target_ref, 0, 2)]) }",
|
||||
)?;
|
||||
let actor = runtime.spawn(code, 5, capacity(1))?;
|
||||
runtime.send(
|
||||
actor,
|
||||
Message {
|
||||
kind: 0,
|
||||
value: i64::try_from(actor.0)?,
|
||||
value: 0,
|
||||
target_ref: Some(actor),
|
||||
},
|
||||
)?;
|
||||
for _ in 0..100 {
|
||||
|
|
@ -704,9 +821,19 @@ mod tests {
|
|||
#[test]
|
||||
fn invalid_transition_shape_fails_without_committing() -> ExperimentResult<()> {
|
||||
let mut runtime = Runtime::default();
|
||||
let code = publish(&mut runtime, "pub fn handle(state, kind, value) { 99 }")?;
|
||||
let code = publish(
|
||||
&mut runtime,
|
||||
"pub fn handle(state, kind, value, target_ref) { 99 }",
|
||||
)?;
|
||||
let actor = runtime.spawn(code, 5, capacity(1))?;
|
||||
runtime.send(actor, Message { kind: 0, value: 1 })?;
|
||||
runtime.send(
|
||||
actor,
|
||||
Message {
|
||||
kind: 0,
|
||||
value: 1,
|
||||
target_ref: None,
|
||||
},
|
||||
)?;
|
||||
for _ in 0..100 {
|
||||
runtime.tick(capacity(5));
|
||||
}
|
||||
|
|
@ -720,16 +847,25 @@ mod tests {
|
|||
#[test]
|
||||
fn behavior_installation_waits_for_handler_boundary() -> ExperimentResult<()> {
|
||||
let mut code = CodeStore::default();
|
||||
let first = code.compile("pub fn handle(state, kind, value) { (state + value, []) }")?;
|
||||
let second =
|
||||
code.compile("pub fn handle(state, kind, value) { (state + value * 10, []) }")?;
|
||||
let first =
|
||||
code.compile("pub fn handle(state, kind, value, target_ref) { (state + value, []) }")?;
|
||||
let second = code.compile(
|
||||
"pub fn handle(state, kind, value, target_ref) { (state + value * 10, []) }",
|
||||
)?;
|
||||
let first_id = first.implementation_id;
|
||||
let second_id = second.implementation_id;
|
||||
let mut runtime = Runtime::default();
|
||||
runtime.publish(first)?;
|
||||
runtime.publish(second)?;
|
||||
let actor = runtime.spawn(first_id, 0, capacity(2))?;
|
||||
runtime.send(actor, Message { kind: 0, value: 1 })?;
|
||||
runtime.send(
|
||||
actor,
|
||||
Message {
|
||||
kind: 0,
|
||||
value: 1,
|
||||
target_ref: None,
|
||||
},
|
||||
)?;
|
||||
runtime.tick(capacity(1));
|
||||
let active = runtime.inspect(actor)?;
|
||||
assert_eq!(active.implementation, first_id);
|
||||
|
|
@ -738,7 +874,14 @@ mod tests {
|
|||
runtime.tick(capacity(10));
|
||||
}
|
||||
assert_eq!(runtime.inspect(actor)?.state, 1);
|
||||
runtime.send(actor, Message { kind: 0, value: 2 })?;
|
||||
runtime.send(
|
||||
actor,
|
||||
Message {
|
||||
kind: 0,
|
||||
value: 2,
|
||||
target_ref: None,
|
||||
},
|
||||
)?;
|
||||
runtime.install(actor, second_id)?;
|
||||
assert_eq!(runtime.inspect(actor)?.implementation, second_id);
|
||||
for _ in 0..100 {
|
||||
|
|
@ -761,11 +904,11 @@ mod tests {
|
|||
let mut runtime = Runtime::default();
|
||||
let old_implementation = publish(
|
||||
&mut runtime,
|
||||
"pub fn handle(state, kind, value) { (state + value, []) }",
|
||||
"pub fn handle(state, kind, value, target_ref) { (state + value, []) }",
|
||||
)?;
|
||||
let new_implementation = publish(
|
||||
&mut runtime,
|
||||
"pub fn handle(state, kind, value) { (state * value, []) }",
|
||||
"pub fn handle(state, kind, value, target_ref) { (state * value, []) }",
|
||||
)?;
|
||||
let actor = runtime.spawn(old_implementation, 7, capacity(2))?;
|
||||
|
||||
|
|
@ -790,15 +933,29 @@ mod tests {
|
|||
let mut runtime = Runtime::default();
|
||||
let old_implementation = publish(
|
||||
&mut runtime,
|
||||
"pub fn handle(state, kind, value) { (state + value, []) }",
|
||||
"pub fn handle(state, kind, value, target_ref) { (state + value, []) }",
|
||||
)?;
|
||||
let new_implementation = publish(
|
||||
&mut runtime,
|
||||
"pub fn handle(state, kind, value) { (state * 10 + value, []) }",
|
||||
"pub fn handle(state, kind, value, target_ref) { (state * 10 + value, []) }",
|
||||
)?;
|
||||
let actor = runtime.spawn(old_implementation, 0, capacity(4))?;
|
||||
runtime.send(actor, Message { kind: 0, value: 1 })?;
|
||||
runtime.send(actor, Message { kind: 0, value: 2 })?;
|
||||
runtime.send(
|
||||
actor,
|
||||
Message {
|
||||
kind: 0,
|
||||
value: 1,
|
||||
target_ref: None,
|
||||
},
|
||||
)?;
|
||||
runtime.send(
|
||||
actor,
|
||||
Message {
|
||||
kind: 0,
|
||||
value: 2,
|
||||
target_ref: None,
|
||||
},
|
||||
)?;
|
||||
runtime.tick(capacity(1));
|
||||
|
||||
assert_eq!(runtime.inspect(actor)?.status, Status::Running);
|
||||
|
|
@ -808,12 +965,30 @@ mod tests {
|
|||
assert_eq!(cancelled.state, 0);
|
||||
assert_eq!(
|
||||
cancelled.queued,
|
||||
vec![Message { kind: 0, value: 1 }, Message { kind: 0, value: 2 }]
|
||||
vec![
|
||||
Message {
|
||||
kind: 0,
|
||||
value: 1,
|
||||
target_ref: None
|
||||
},
|
||||
Message {
|
||||
kind: 0,
|
||||
value: 2,
|
||||
target_ref: None
|
||||
}
|
||||
]
|
||||
);
|
||||
assert!(cancelled.reserved.is_none());
|
||||
|
||||
runtime.install(actor, new_implementation)?;
|
||||
runtime.send(actor, Message { kind: 0, value: 3 })?;
|
||||
runtime.send(
|
||||
actor,
|
||||
Message {
|
||||
kind: 0,
|
||||
value: 3,
|
||||
target_ref: None,
|
||||
},
|
||||
)?;
|
||||
for _ in 0..100 {
|
||||
runtime.tick(capacity(10));
|
||||
}
|
||||
|
|
@ -834,11 +1009,11 @@ mod tests {
|
|||
let mut runtime = Runtime::default();
|
||||
let old_implementation = publish(
|
||||
&mut runtime,
|
||||
"pub fn handle(state, kind, value) { (state + value, []) }",
|
||||
"pub fn handle(state, kind, value, target_ref) { (state + value, []) }",
|
||||
)?;
|
||||
let new_implementation = publish(
|
||||
&mut runtime,
|
||||
"pub fn handle(state, kind, value) { (state * value, []) }",
|
||||
"pub fn handle(state, kind, value, target_ref) { (state * value, []) }",
|
||||
)?;
|
||||
let actor = runtime.spawn(old_implementation, 7, capacity(2))?;
|
||||
|
||||
|
|
@ -869,14 +1044,21 @@ mod tests {
|
|||
let mut runtime = Runtime::default();
|
||||
let old_implementation = publish(
|
||||
&mut runtime,
|
||||
"pub fn handle(state, kind, value) { (state + value, []) }",
|
||||
"pub fn handle(state, kind, value, target_ref) { (state + value, []) }",
|
||||
)?;
|
||||
let new_implementation = publish(
|
||||
&mut runtime,
|
||||
"pub fn handle(state, kind, value) { (state * value, []) }",
|
||||
"pub fn handle(state, kind, value, target_ref) { (state * value, []) }",
|
||||
)?;
|
||||
let actor = runtime.spawn(old_implementation, 7, capacity(2))?;
|
||||
runtime.send(actor, Message { kind: 0, value: 1 })?;
|
||||
runtime.send(
|
||||
actor,
|
||||
Message {
|
||||
kind: 0,
|
||||
value: 1,
|
||||
target_ref: None,
|
||||
},
|
||||
)?;
|
||||
runtime.tick(capacity(1));
|
||||
let called = Cell::new(false);
|
||||
|
||||
|
|
@ -908,14 +1090,22 @@ mod tests {
|
|||
let mut runtime = Runtime::default();
|
||||
let code = runtime.publish(
|
||||
CodeStore::default().compile_with_module(
|
||||
"fn check() { if fail_once() { 1 / 0 } else { 0 } } pub fn handle(state, kind, value) { let _ = check(); (state + value, []) }",
|
||||
"fn check() { if fail_once() { 1 / 0 } else { 0 } } pub fn handle(state, kind, value, target_ref) { let _ = check(); (state + value, []) }",
|
||||
"test-fail-once:v1",
|
||||
module,
|
||||
)?,
|
||||
)?;
|
||||
let actor = runtime.spawn(code, 10, capacity(3))?;
|
||||
let first = Message { kind: 1, value: 2 };
|
||||
let second = Message { kind: 1, value: 3 };
|
||||
let first = Message {
|
||||
kind: 1,
|
||||
value: 2,
|
||||
target_ref: None,
|
||||
};
|
||||
let second = Message {
|
||||
kind: 1,
|
||||
value: 3,
|
||||
target_ref: None,
|
||||
};
|
||||
runtime.send(actor, first)?;
|
||||
runtime.send(actor, second)?;
|
||||
for _ in 0..100 {
|
||||
|
|
@ -927,7 +1117,18 @@ mod tests {
|
|||
assert_eq!(failed.state, 10);
|
||||
assert_eq!(failed.reserved, Some(first));
|
||||
assert_eq!(failed.queued, vec![second]);
|
||||
assert!(runtime.send(actor, Message { kind: 1, value: 4 }).is_err());
|
||||
assert!(
|
||||
runtime
|
||||
.send(
|
||||
actor,
|
||||
Message {
|
||||
kind: 1,
|
||||
value: 4,
|
||||
target_ref: None
|
||||
}
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
runtime
|
||||
.drain_events()
|
||||
|
|
@ -937,7 +1138,7 @@ mod tests {
|
|||
|
||||
runtime.restart(actor)?;
|
||||
let restarted = runtime.inspect(actor)?;
|
||||
assert_eq!(restarted.id, failed.id);
|
||||
assert_eq!(restarted.actor_ref, failed.actor_ref);
|
||||
assert_eq!(restarted.mailbox, failed.mailbox);
|
||||
assert_eq!(restarted.implementation, failed.implementation);
|
||||
assert_eq!(restarted.incarnation, failed.incarnation + 1);
|
||||
|
|
@ -961,12 +1162,24 @@ mod tests {
|
|||
let mut runtime = Runtime::default();
|
||||
let code = publish(
|
||||
&mut runtime,
|
||||
"pub fn handle(state, kind, value) { (state * 10 + value, []) }",
|
||||
"pub fn handle(state, kind, value, target_ref) { (state * 10 + value, []) }",
|
||||
)?;
|
||||
let actor = runtime.spawn(code, 0, capacity(3))?;
|
||||
let first = Message { kind: 0, value: 1 };
|
||||
let second = Message { kind: 0, value: 2 };
|
||||
let third = Message { kind: 0, value: 3 };
|
||||
let first = Message {
|
||||
kind: 0,
|
||||
value: 1,
|
||||
target_ref: None,
|
||||
};
|
||||
let second = Message {
|
||||
kind: 0,
|
||||
value: 2,
|
||||
target_ref: None,
|
||||
};
|
||||
let third = Message {
|
||||
kind: 0,
|
||||
value: 3,
|
||||
target_ref: None,
|
||||
};
|
||||
runtime.send(actor, first)?;
|
||||
runtime.send(actor, second)?;
|
||||
runtime.send(actor, third)?;
|
||||
|
|
@ -984,7 +1197,18 @@ mod tests {
|
|||
assert_eq!(stopped.state, 0);
|
||||
assert_eq!(stopped.reserved, Some(first));
|
||||
assert_eq!(stopped.queued, vec![second, third]);
|
||||
assert!(runtime.send(actor, Message { kind: 0, value: 4 }).is_err());
|
||||
assert!(
|
||||
runtime
|
||||
.send(
|
||||
actor,
|
||||
Message {
|
||||
kind: 0,
|
||||
value: 4,
|
||||
target_ref: None
|
||||
}
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
|
||||
runtime.restart(actor)?;
|
||||
let restarted = runtime.inspect(actor)?;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use std::num::NonZeroUsize;
|
|||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use runa_actor::{ActorId, Event, Message, Status};
|
||||
use runa_actor::{ActorRef, Event, Message, Status};
|
||||
use runa_image_api::ImageApi;
|
||||
use runa_protocol::{
|
||||
ActorSnapshotDto, ActorStatusDto, BehaviorDto, BehaviorVersionDto, Command, ErrorCode,
|
||||
|
|
@ -339,16 +339,18 @@ impl Server {
|
|||
.ok_or_else(|| invalid_request("mailbox_capacity must be positive"))?;
|
||||
self.image
|
||||
.spawn(&behavior_name, state, capacity)
|
||||
.map(|actor_id| Reply::ActorSpawned {
|
||||
actor_id: actor_id.0,
|
||||
.map(|actor_ref| Reply::ActorSpawned {
|
||||
actor_ref: actor_ref.as_str().to_owned(),
|
||||
})
|
||||
.map_err(api_error)
|
||||
}
|
||||
Command::Send { actor_id, message } => self
|
||||
.image
|
||||
.send(ActorId(actor_id), message_from_dto(message))
|
||||
Command::Send { actor_ref, message } => {
|
||||
let actor_ref = parse_actor_ref(&actor_ref)?;
|
||||
self.image
|
||||
.send(actor_ref, message_from_dto(message)?)
|
||||
.map(|()| Reply::Acknowledged)
|
||||
.map_err(api_error),
|
||||
.map_err(api_error)
|
||||
}
|
||||
Command::Tick {
|
||||
ticks,
|
||||
instruction_budget,
|
||||
|
|
@ -358,53 +360,67 @@ impl Server {
|
|||
self.image.tick(ticks, budget);
|
||||
Ok(Reply::TickCompleted { ticks })
|
||||
}
|
||||
Command::InspectActor { actor_id } => self
|
||||
.image
|
||||
.inspect(ActorId(actor_id))
|
||||
Command::InspectActor { actor_ref } => {
|
||||
let actor_ref = parse_actor_ref(&actor_ref)?;
|
||||
self.image
|
||||
.inspect(actor_ref)
|
||||
.map(|snapshot| actor_snapshot_dto(&snapshot))
|
||||
.map(Reply::Actor)
|
||||
.map_err(api_error),
|
||||
.map_err(api_error)
|
||||
}
|
||||
Command::ListActors => Ok(Reply::Actors(
|
||||
self.image.actors().iter().map(actor_snapshot_dto).collect(),
|
||||
)),
|
||||
Command::GetEvents => Ok(Reply::Events(
|
||||
self.image.events().iter().map(event_dto).collect(),
|
||||
)),
|
||||
Command::StopActor { actor_id } => self
|
||||
.image
|
||||
.stop(ActorId(actor_id))
|
||||
Command::StopActor { actor_ref } => {
|
||||
let actor_ref = parse_actor_ref(&actor_ref)?;
|
||||
self.image
|
||||
.stop(actor_ref)
|
||||
.map(|()| Reply::Acknowledged)
|
||||
.map_err(api_error),
|
||||
Command::RestartActor { actor_id } => self
|
||||
.image
|
||||
.restart(ActorId(actor_id))
|
||||
.map_err(api_error)
|
||||
}
|
||||
Command::RestartActor { actor_ref } => {
|
||||
let actor_ref = parse_actor_ref(&actor_ref)?;
|
||||
self.image
|
||||
.restart(actor_ref)
|
||||
.map(|()| Reply::Acknowledged)
|
||||
.map_err(api_error),
|
||||
Command::PauseActor { actor_id } => self
|
||||
.image
|
||||
.pause(ActorId(actor_id))
|
||||
.map_err(api_error)
|
||||
}
|
||||
Command::PauseActor { actor_ref } => {
|
||||
let actor_ref = parse_actor_ref(&actor_ref)?;
|
||||
self.image
|
||||
.pause(actor_ref)
|
||||
.map(|()| Reply::Acknowledged)
|
||||
.map_err(api_error),
|
||||
Command::ResumeActor { actor_id } => self
|
||||
.image
|
||||
.resume(ActorId(actor_id))
|
||||
.map_err(api_error)
|
||||
}
|
||||
Command::ResumeActor { actor_ref } => {
|
||||
let actor_ref = parse_actor_ref(&actor_ref)?;
|
||||
self.image
|
||||
.resume(actor_ref)
|
||||
.map(|()| Reply::Acknowledged)
|
||||
.map_err(api_error),
|
||||
Command::CancelActor { actor_id } => self
|
||||
.image
|
||||
.cancel(ActorId(actor_id))
|
||||
.map_err(api_error)
|
||||
}
|
||||
Command::CancelActor { actor_ref } => {
|
||||
let actor_ref = parse_actor_ref(&actor_ref)?;
|
||||
self.image
|
||||
.cancel(actor_ref)
|
||||
.map(|()| Reply::Acknowledged)
|
||||
.map_err(api_error),
|
||||
.map_err(api_error)
|
||||
}
|
||||
Command::InstallBehavior {
|
||||
actor_id,
|
||||
actor_ref,
|
||||
behavior_name,
|
||||
} => self
|
||||
.image
|
||||
.install(ActorId(actor_id), &behavior_name)
|
||||
} => {
|
||||
let actor_ref = parse_actor_ref(&actor_ref)?;
|
||||
self.image
|
||||
.install(actor_ref, &behavior_name)
|
||||
.map(|implementation_id| Reply::BehaviorInstalled {
|
||||
implementation_id: implementation_id.to_string(),
|
||||
})
|
||||
.map_err(api_error),
|
||||
.map_err(api_error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -427,7 +443,7 @@ fn behavior_dto(behavior: runa_code::BehaviorSnapshot) -> BehaviorDto {
|
|||
|
||||
fn actor_snapshot_dto(snapshot: &runa_actor::ActorSnapshot) -> ActorSnapshotDto {
|
||||
ActorSnapshotDto {
|
||||
actor_id: snapshot.id.0,
|
||||
actor_ref: snapshot.actor_ref.as_str().to_owned(),
|
||||
mailbox_id: snapshot.mailbox.0,
|
||||
implementation_id: snapshot.implementation.to_string(),
|
||||
incarnation: snapshot.incarnation,
|
||||
|
|
@ -454,49 +470,57 @@ fn message_dto(message: Message) -> MessageDto {
|
|||
MessageDto {
|
||||
kind: message.kind,
|
||||
value: message.value,
|
||||
target_ref: message
|
||||
.target_ref
|
||||
.map(|actor_ref| actor_ref.as_str().to_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
fn message_from_dto(message: MessageDto) -> Message {
|
||||
Message {
|
||||
fn message_from_dto(message: MessageDto) -> Result<Message, ProtocolError> {
|
||||
Ok(Message {
|
||||
kind: message.kind,
|
||||
value: message.value,
|
||||
}
|
||||
target_ref: message
|
||||
.target_ref
|
||||
.as_deref()
|
||||
.map(parse_actor_ref)
|
||||
.transpose()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn event_dto(event: &Event) -> EventDto {
|
||||
match event {
|
||||
Event::Spawned(actor_id) => EventDto::Spawned {
|
||||
actor_id: actor_id.0,
|
||||
Event::Spawned(actor_ref) => EventDto::Spawned {
|
||||
actor_ref: actor_ref.as_str().to_owned(),
|
||||
},
|
||||
Event::Committed(actor_id) => EventDto::Committed {
|
||||
actor_id: actor_id.0,
|
||||
Event::Committed(actor_ref) => EventDto::Committed {
|
||||
actor_ref: actor_ref.as_str().to_owned(),
|
||||
},
|
||||
Event::Failed(actor_id, message) => EventDto::Failed {
|
||||
actor_id: actor_id.0,
|
||||
Event::Failed(actor_ref, message) => EventDto::Failed {
|
||||
actor_ref: actor_ref.as_str().to_owned(),
|
||||
message: message.clone(),
|
||||
},
|
||||
Event::Stopped(actor_id) => EventDto::Stopped {
|
||||
actor_id: actor_id.0,
|
||||
Event::Stopped(actor_ref) => EventDto::Stopped {
|
||||
actor_ref: actor_ref.as_str().to_owned(),
|
||||
},
|
||||
Event::Restarted(actor_id) => EventDto::Restarted {
|
||||
actor_id: actor_id.0,
|
||||
Event::Restarted(actor_ref) => EventDto::Restarted {
|
||||
actor_ref: actor_ref.as_str().to_owned(),
|
||||
},
|
||||
Event::Paused(actor_id) => EventDto::Paused {
|
||||
actor_id: actor_id.0,
|
||||
Event::Paused(actor_ref) => EventDto::Paused {
|
||||
actor_ref: actor_ref.as_str().to_owned(),
|
||||
},
|
||||
Event::Resumed(actor_id) => EventDto::Resumed {
|
||||
actor_id: actor_id.0,
|
||||
Event::Resumed(actor_ref) => EventDto::Resumed {
|
||||
actor_ref: actor_ref.as_str().to_owned(),
|
||||
},
|
||||
Event::InvocationCancelled(actor_id) => EventDto::InvocationCancelled {
|
||||
actor_id: actor_id.0,
|
||||
Event::InvocationCancelled(actor_ref) => EventDto::InvocationCancelled {
|
||||
actor_ref: actor_ref.as_str().to_owned(),
|
||||
},
|
||||
Event::BehaviorInstalled(actor_id, implementation_id) => EventDto::BehaviorInstalled {
|
||||
actor_id: actor_id.0,
|
||||
Event::BehaviorInstalled(actor_ref, implementation_id) => EventDto::BehaviorInstalled {
|
||||
actor_ref: actor_ref.as_str().to_owned(),
|
||||
implementation_id: implementation_id.to_string(),
|
||||
},
|
||||
Event::StateMigrated(actor_id, previous_state, current_state) => EventDto::StateMigrated {
|
||||
actor_id: actor_id.0,
|
||||
Event::StateMigrated(actor_ref, previous_state, current_state) => EventDto::StateMigrated {
|
||||
actor_ref: actor_ref.as_str().to_owned(),
|
||||
previous_state: *previous_state,
|
||||
current_state: *current_state,
|
||||
},
|
||||
|
|
@ -533,6 +557,10 @@ fn invalid_request(message: impl Into<String>) -> ProtocolError {
|
|||
ProtocolError::new(ErrorCode::InvalidRequest, message)
|
||||
}
|
||||
|
||||
fn parse_actor_ref(value: &str) -> Result<ActorRef, ProtocolError> {
|
||||
ActorRef::parse(value).ok_or_else(|| invalid_request("actor_ref must be a valid Nano ID"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -552,7 +580,8 @@ mod tests {
|
|||
let response = server.dispatch(request(Command::PublishBehavior {
|
||||
name: "counter".to_owned(),
|
||||
expected_revision: None,
|
||||
source: "pub fn handle(state, kind, value) { (state + value, []) }".to_owned(),
|
||||
source: "pub fn handle(state, kind, value, target_ref) { (state + value, []) }"
|
||||
.to_owned(),
|
||||
}));
|
||||
match response.result.unwrap() {
|
||||
Reply::Behavior(behavior) => behavior,
|
||||
|
|
@ -572,15 +601,20 @@ mod tests {
|
|||
state: 5,
|
||||
mailbox_capacity: 2,
|
||||
}));
|
||||
let actor_id = match spawned.result.unwrap() {
|
||||
Reply::ActorSpawned { actor_id } => actor_id,
|
||||
let actor_ref = match spawned.result.unwrap() {
|
||||
Reply::ActorSpawned { actor_ref } => actor_ref,
|
||||
other => panic!("unexpected reply: {other:?}"),
|
||||
};
|
||||
assert_eq!(actor_id, 1);
|
||||
assert_eq!(actor_ref.len(), 21);
|
||||
assert!(!actor_ref.parse::<u64>().is_ok());
|
||||
|
||||
let sent = server.dispatch(request(Command::Send {
|
||||
actor_id,
|
||||
message: MessageDto { kind: 0, value: 7 },
|
||||
actor_ref: actor_ref.clone(),
|
||||
message: MessageDto {
|
||||
kind: 0,
|
||||
value: 7,
|
||||
target_ref: None,
|
||||
},
|
||||
}));
|
||||
assert_eq!(sent.result, Ok(Reply::Acknowledged));
|
||||
server.dispatch(request(Command::Tick {
|
||||
|
|
@ -588,7 +622,9 @@ mod tests {
|
|||
instruction_budget: 10,
|
||||
}));
|
||||
|
||||
let inspected = server.dispatch(request(Command::InspectActor { actor_id }));
|
||||
let inspected = server.dispatch(request(Command::InspectActor {
|
||||
actor_ref: actor_ref.clone(),
|
||||
}));
|
||||
match inspected.result.unwrap() {
|
||||
Reply::Actor(snapshot) => assert_eq!(snapshot.state, 12),
|
||||
other => panic!("unexpected reply: {other:?}"),
|
||||
|
|
@ -597,7 +633,7 @@ mod tests {
|
|||
let events = server.dispatch(request(Command::GetEvents));
|
||||
match events.result.unwrap() {
|
||||
Reply::Events(events) => assert!(events.iter().any(
|
||||
|event| matches!(event, EventDto::Committed { actor_id: id } if *id == actor_id)
|
||||
|event| matches!(event, EventDto::Committed { actor_ref: id } if id == &actor_ref)
|
||||
)),
|
||||
other => panic!("unexpected reply: {other:?}"),
|
||||
}
|
||||
|
|
@ -619,14 +655,16 @@ mod tests {
|
|||
ErrorCode::UnsupportedVersion
|
||||
);
|
||||
|
||||
let missing = server.dispatch(request(Command::InspectActor { actor_id: 42 }));
|
||||
let missing = server.dispatch(request(Command::InspectActor {
|
||||
actor_ref: "000000000000000000000".to_owned(),
|
||||
}));
|
||||
assert_eq!(missing.result.unwrap_err().code, ErrorCode::NotFound);
|
||||
|
||||
publish(&mut server);
|
||||
let conflict = server.dispatch(request(Command::PublishBehavior {
|
||||
name: "counter".to_owned(),
|
||||
expected_revision: Some(99),
|
||||
source: "pub fn handle(state, kind, value) { (state, []) }".to_owned(),
|
||||
source: "pub fn handle(state, kind, value, target_ref) { (state, []) }".to_owned(),
|
||||
}));
|
||||
assert_eq!(conflict.result.unwrap_err().code, ErrorCode::Conflict);
|
||||
}
|
||||
|
|
@ -638,7 +676,8 @@ mod tests {
|
|||
let updated = server.dispatch(request(Command::PublishBehavior {
|
||||
name: "counter".to_owned(),
|
||||
expected_revision: Some(first.revision),
|
||||
source: "pub fn handle(state, kind, value) { (state * value, []) }".to_owned(),
|
||||
source: "pub fn handle(state, kind, value, target_ref) { (state * value, []) }"
|
||||
.to_owned(),
|
||||
}));
|
||||
assert!(matches!(updated.result, Ok(Reply::Behavior(_))));
|
||||
|
||||
|
|
@ -649,11 +688,12 @@ mod tests {
|
|||
assert_eq!(
|
||||
source.result,
|
||||
Ok(Reply::BehaviorSource {
|
||||
source: "pub fn handle(state, kind, value) { (state + value, []) }".to_owned()
|
||||
source: "pub fn handle(state, kind, value, target_ref) { (state + value, []) }"
|
||||
.to_owned()
|
||||
})
|
||||
);
|
||||
|
||||
let actor_id = match server
|
||||
let actor_ref = match server
|
||||
.dispatch(request(Command::Spawn {
|
||||
behavior_name: "counter".to_owned(),
|
||||
state: 3,
|
||||
|
|
@ -662,11 +702,11 @@ mod tests {
|
|||
.result
|
||||
.unwrap()
|
||||
{
|
||||
Reply::ActorSpawned { actor_id } => actor_id,
|
||||
Reply::ActorSpawned { actor_ref } => actor_ref,
|
||||
other => panic!("unexpected reply: {other:?}"),
|
||||
};
|
||||
let installed = server.dispatch(request(Command::InstallBehavior {
|
||||
actor_id,
|
||||
actor_ref,
|
||||
behavior_name: "counter".to_owned(),
|
||||
}));
|
||||
assert!(matches!(
|
||||
|
|
@ -680,7 +720,8 @@ mod tests {
|
|||
let publish_request = request(Command::PublishBehavior {
|
||||
name: "counter".to_owned(),
|
||||
expected_revision: None,
|
||||
source: "pub fn handle(state, kind, value) { (state + value, []) }".to_owned(),
|
||||
source: "pub fn handle(state, kind, value, target_ref) { (state + value, []) }"
|
||||
.to_owned(),
|
||||
});
|
||||
let mut spawn_request = request(Command::Spawn {
|
||||
behavior_name: "counter".to_owned(),
|
||||
|
|
@ -717,7 +758,10 @@ mod tests {
|
|||
vec!["test-request", "spawn-request", "list-request"]
|
||||
);
|
||||
assert!(matches!(responses[0].result, Ok(Reply::Behavior(_))));
|
||||
assert_eq!(responses[1].result, Ok(Reply::ActorSpawned { actor_id: 1 }));
|
||||
assert!(matches!(
|
||||
&responses[1].result,
|
||||
Ok(Reply::ActorSpawned { actor_ref }) if actor_ref.len() == 21
|
||||
));
|
||||
assert!(matches!(
|
||||
&responses[2].result,
|
||||
Ok(Reply::Actors(actors)) if actors.len() == 1 && actors[0].state == 4
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@ fn local_client_launches_server_and_operates_the_same_live_image() {
|
|||
.request(Command::PublishBehavior {
|
||||
name: "counter".to_owned(),
|
||||
expected_revision: None,
|
||||
source: "pub fn handle(state, kind, value) { (state + value, []) }".to_owned(),
|
||||
source: "pub fn handle(state, kind, value, target_ref) { (state + value, []) }"
|
||||
.to_owned(),
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(published.request_id, "runa-client-1");
|
||||
|
|
@ -37,12 +38,20 @@ fn local_client_launches_server_and_operates_the_same_live_image() {
|
|||
})
|
||||
.unwrap();
|
||||
assert_eq!(spawned.request_id, "runa-client-2");
|
||||
assert_eq!(spawned.result, Ok(Reply::ActorSpawned { actor_id: 1 }));
|
||||
let actor_ref = match spawned.result {
|
||||
Ok(Reply::ActorSpawned { actor_ref }) => actor_ref,
|
||||
other => panic!("unexpected spawn response: {other:?}"),
|
||||
};
|
||||
assert_eq!(actor_ref.len(), 21);
|
||||
|
||||
client
|
||||
.request(Command::Send {
|
||||
actor_id: 1,
|
||||
message: MessageDto { kind: 0, value: 7 },
|
||||
actor_ref: actor_ref.clone(),
|
||||
message: MessageDto {
|
||||
kind: 0,
|
||||
value: 7,
|
||||
target_ref: None,
|
||||
},
|
||||
})
|
||||
.unwrap();
|
||||
client
|
||||
|
|
@ -51,9 +60,7 @@ fn local_client_launches_server_and_operates_the_same_live_image() {
|
|||
instruction_budget: 10,
|
||||
})
|
||||
.unwrap();
|
||||
let inspected = client
|
||||
.request(Command::InspectActor { actor_id: 1 })
|
||||
.unwrap();
|
||||
let inspected = client.request(Command::InspectActor { actor_ref }).unwrap();
|
||||
assert!(matches!(
|
||||
inspected.result,
|
||||
Ok(Reply::Actor(actor)) if actor.state == 12
|
||||
|
|
|
|||
|
|
@ -6,3 +6,4 @@ edition = "2024"
|
|||
[dependencies]
|
||||
runa-protocol = { path = "../runa-protocol" }
|
||||
runa-server = { path = "../runa-server" }
|
||||
rustyline = "15.0"
|
||||
|
|
|
|||
|
|
@ -1,19 +1,144 @@
|
|||
use std::io::{self, BufRead, Write};
|
||||
use std::collections::BTreeMap;
|
||||
#[cfg(test)]
|
||||
use std::io::BufRead;
|
||||
use std::io::{self, Write};
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use runa_protocol::{Command, MessageDto, PROTOCOL_VERSION, Reply, Request, Response};
|
||||
use runa_server::Server;
|
||||
use rustyline::completion::{Completer, Pair};
|
||||
use rustyline::error::ReadlineError;
|
||||
use rustyline::highlight::Highlighter;
|
||||
use rustyline::hint::Hinter;
|
||||
use rustyline::history::DefaultHistory;
|
||||
use rustyline::validate::Validator;
|
||||
use rustyline::{Context, Editor, Helper};
|
||||
|
||||
const MAILBOX_CAPACITY: usize = 64;
|
||||
const DEFAULT_TICK_BUDGET: usize = 32;
|
||||
const DEFAULT_BEHAVIOR: &str = "counter";
|
||||
const COUNTER_BEHAVIOR: &str = "counter";
|
||||
|
||||
#[derive(Default)]
|
||||
struct ReplState {
|
||||
bindings: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ReplHelper {
|
||||
behaviors: Vec<String>,
|
||||
bindings: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
impl ReplHelper {
|
||||
fn set_context(&mut self, behaviors: Vec<String>, bindings: &BTreeMap<String, String>) {
|
||||
self.behaviors = behaviors;
|
||||
self.bindings = bindings
|
||||
.iter()
|
||||
.map(|(name, actor_ref)| (name.clone(), actor_ref.clone()))
|
||||
.collect();
|
||||
}
|
||||
|
||||
fn candidates(&self, line: &str, position: usize) -> Vec<String> {
|
||||
const COMMANDS: &[&str] = &[
|
||||
"actors",
|
||||
"behaviors",
|
||||
"cancel",
|
||||
"events",
|
||||
"exit",
|
||||
"help",
|
||||
"history",
|
||||
"inspect",
|
||||
"install",
|
||||
"let",
|
||||
"pause",
|
||||
"publish",
|
||||
"quit",
|
||||
"restart",
|
||||
"resume",
|
||||
"rollback",
|
||||
"send",
|
||||
"spawn",
|
||||
"stop",
|
||||
"tick",
|
||||
];
|
||||
let start = line[..position]
|
||||
.rfind(char::is_whitespace)
|
||||
.map_or(0, |index| index + 1);
|
||||
let previous: Vec<_> = line[..start].split_whitespace().collect();
|
||||
let options: Vec<String> = match previous.as_slice() {
|
||||
[] => COMMANDS
|
||||
.iter()
|
||||
.map(|command| (*command).to_owned())
|
||||
.collect(),
|
||||
["let", _, "="] => vec!["spawn".to_owned()],
|
||||
["spawn"] | ["let", _, "=", "spawn"] => self.behaviors.clone(),
|
||||
["let", _, "=", "spawn", _] => Vec::new(),
|
||||
["send"]
|
||||
| ["inspect"]
|
||||
| ["stop"]
|
||||
| ["restart"]
|
||||
| ["pause"]
|
||||
| ["resume"]
|
||||
| ["cancel"]
|
||||
| ["install"] => self.actor_candidates(),
|
||||
["send", _, _, _] => vec!["--to".to_owned()],
|
||||
["send", _, _, _, "--to"] => self.actor_candidates(),
|
||||
["install", _] => self.behaviors.clone(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
let prefix = &line[start..position];
|
||||
options
|
||||
.into_iter()
|
||||
.filter(|option| option.starts_with(prefix))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn actor_candidates(&self) -> Vec<String> {
|
||||
self.bindings
|
||||
.iter()
|
||||
.flat_map(|(name, actor_ref)| [name.clone(), format!("@{actor_ref}")])
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Completer for ReplHelper {
|
||||
type Candidate = Pair;
|
||||
|
||||
fn complete(
|
||||
&self,
|
||||
line: &str,
|
||||
position: usize,
|
||||
_context: &Context<'_>,
|
||||
) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
|
||||
let start = line[..position]
|
||||
.rfind(char::is_whitespace)
|
||||
.map_or(0, |index| index + 1);
|
||||
let candidates = self
|
||||
.candidates(line, position)
|
||||
.into_iter()
|
||||
.map(|candidate| Pair {
|
||||
display: candidate.clone(),
|
||||
replacement: candidate,
|
||||
})
|
||||
.collect();
|
||||
Ok((start, candidates))
|
||||
}
|
||||
}
|
||||
|
||||
impl Hinter for ReplHelper {
|
||||
type Hint = String;
|
||||
}
|
||||
|
||||
impl Highlighter for ReplHelper {}
|
||||
impl Validator for ReplHelper {}
|
||||
impl Helper for ReplHelper {}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut server = Server::new();
|
||||
let behavior = match dispatch(
|
||||
&mut server,
|
||||
Command::PublishBehavior {
|
||||
name: DEFAULT_BEHAVIOR.to_owned(),
|
||||
name: COUNTER_BEHAVIOR.to_owned(),
|
||||
expected_revision: None,
|
||||
source: include_str!("../../../runa-std/actors/counter.rn").to_owned(),
|
||||
},
|
||||
|
|
@ -27,22 +152,18 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
"Loaded behavior `{}` revision {}.",
|
||||
behavior.name, behavior.revision
|
||||
);
|
||||
println!("Type `help` for commands.");
|
||||
run_repl(
|
||||
io::stdin().lock(),
|
||||
io::stdout().lock(),
|
||||
&mut server,
|
||||
DEFAULT_BEHAVIOR,
|
||||
)?;
|
||||
println!("Type `help` for commands. Use Up/Down for history and Tab for completion.");
|
||||
run_interactive_repl(&mut server)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn run_repl<R: BufRead, W: Write>(
|
||||
mut input: R,
|
||||
mut output: W,
|
||||
server: &mut Server,
|
||||
default_behavior: &str,
|
||||
) -> io::Result<()> {
|
||||
let mut state = ReplState::default();
|
||||
let mut line = String::new();
|
||||
loop {
|
||||
write!(output, "runa> ")?;
|
||||
|
|
@ -52,14 +173,83 @@ fn run_repl<R: BufRead, W: Write>(
|
|||
writeln!(output)?;
|
||||
break;
|
||||
}
|
||||
let mut words = line.split_whitespace();
|
||||
let Some(command) = words.next() else {
|
||||
continue;
|
||||
if !execute_line(&line, &mut output, server, &mut state)? {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_interactive_repl(server: &mut Server) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut editor = Editor::<ReplHelper, DefaultHistory>::new()?;
|
||||
editor.set_helper(Some(ReplHelper::default()));
|
||||
let mut state = ReplState::default();
|
||||
let mut output = io::stdout().lock();
|
||||
loop {
|
||||
let behaviors = match dispatch(server, Command::GetBehaviors)? {
|
||||
Reply::Behaviors(behaviors) => behaviors
|
||||
.into_iter()
|
||||
.map(|behavior| behavior.name)
|
||||
.collect(),
|
||||
other => return Err(format!("unexpected protocol response: {other:?}").into()),
|
||||
};
|
||||
let args: Vec<_> = words.collect();
|
||||
editor
|
||||
.helper_mut()
|
||||
.expect("REPL helper is installed")
|
||||
.set_context(behaviors, &state.bindings);
|
||||
match editor.readline("runa> ") {
|
||||
Ok(line) => {
|
||||
if !line.trim().is_empty() {
|
||||
editor.add_history_entry(line.as_str())?;
|
||||
}
|
||||
if !execute_line(&line, &mut output, server, &mut state)? {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(ReadlineError::Interrupted) => writeln!(output, "^C")?,
|
||||
Err(ReadlineError::Eof) => {
|
||||
writeln!(output)?;
|
||||
break;
|
||||
}
|
||||
Err(error) => return Err(error.into()),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn execute_line<W: Write>(
|
||||
line: &str,
|
||||
output: &mut W,
|
||||
server: &mut Server,
|
||||
state: &mut ReplState,
|
||||
) -> io::Result<bool> {
|
||||
let words: Vec<_> = line.split_whitespace().collect();
|
||||
if words.is_empty() {
|
||||
return Ok(true);
|
||||
}
|
||||
if let ["let", name, "=", "spawn", behavior_name, initial_state] = words.as_slice() {
|
||||
if !is_binding_name(name) {
|
||||
writeln!(output, "error: invalid actor binding name `{name}`")?;
|
||||
return Ok(true);
|
||||
}
|
||||
if state.bindings.contains_key(*name) {
|
||||
writeln!(output, "error: actor binding `{name}` already exists")?;
|
||||
return Ok(true);
|
||||
}
|
||||
match spawn_actor(behavior_name, initial_state, server) {
|
||||
Ok(actor_ref) => {
|
||||
writeln!(output, "{name} = @{actor_ref}")?;
|
||||
state.bindings.insert((*name).to_owned(), actor_ref);
|
||||
}
|
||||
Err(error) => writeln!(output, "error: {error}")?,
|
||||
}
|
||||
return Ok(true);
|
||||
}
|
||||
let command = words[0];
|
||||
let args = &words[1..];
|
||||
let result = match command {
|
||||
"help" => print_help(&mut output),
|
||||
"spawn" => spawn_command(&mut output, &args, default_behavior, server),
|
||||
"help" => print_help(output),
|
||||
"spawn" => spawn_command(output, args, server),
|
||||
"behaviors" if args.is_empty() => match dispatch(server, Command::GetBehaviors) {
|
||||
Ok(Reply::Behaviors(behaviors)) => {
|
||||
for behavior in behaviors {
|
||||
|
|
@ -71,14 +261,14 @@ fn run_repl<R: BufRead, W: Write>(
|
|||
}
|
||||
Ok(())
|
||||
}
|
||||
Ok(other) => unexpected_reply(&mut output, other),
|
||||
Ok(other) => unexpected_reply(output, other),
|
||||
Err(error) => writeln!(output, "error: {error}"),
|
||||
},
|
||||
"behaviors" => writeln!(output, "error: usage: behaviors"),
|
||||
"publish" => publish_command(&mut output, &args, server),
|
||||
"history" => history_command(&mut output, &args, server),
|
||||
"rollback" => rollback_command(&mut output, &args, server),
|
||||
"install" => install_command(&mut output, &args, server),
|
||||
"publish" => publish_command(output, args, server),
|
||||
"history" => history_command(output, args, server),
|
||||
"rollback" => rollback_command(output, args, server),
|
||||
"install" => install_command(output, args, server, state),
|
||||
"actors" if args.is_empty() => match dispatch(server, Command::ListActors) {
|
||||
Ok(Reply::Actors(actors)) => {
|
||||
for actor in actors {
|
||||
|
|
@ -86,27 +276,31 @@ fn run_repl<R: BufRead, W: Write>(
|
|||
}
|
||||
Ok(())
|
||||
}
|
||||
Ok(other) => unexpected_reply(&mut output, other),
|
||||
Ok(other) => unexpected_reply(output, other),
|
||||
Err(error) => writeln!(output, "error: {error}"),
|
||||
},
|
||||
"actors" => writeln!(output, "error: usage: actors"),
|
||||
"send" => match parse_send_args(&args) {
|
||||
Ok((actor, kind, value)) => {
|
||||
"send" => match parse_send_args(args, state) {
|
||||
Ok((actor_ref, kind, value, target_ref)) => {
|
||||
match dispatch(
|
||||
server,
|
||||
Command::Send {
|
||||
actor_id: actor,
|
||||
message: MessageDto { kind, value },
|
||||
actor_ref,
|
||||
message: MessageDto {
|
||||
kind,
|
||||
value,
|
||||
target_ref,
|
||||
},
|
||||
},
|
||||
) {
|
||||
Ok(Reply::Acknowledged) => writeln!(output, "message queued"),
|
||||
Ok(other) => unexpected_reply(&mut output, other),
|
||||
Ok(other) => unexpected_reply(output, other),
|
||||
Err(error) => writeln!(output, "error: {error}"),
|
||||
}
|
||||
}
|
||||
Err(error) => writeln!(output, "error: {error}"),
|
||||
},
|
||||
"tick" => match parse_tick_args(&args) {
|
||||
"tick" => match parse_tick_args(args) {
|
||||
Ok((count, budget)) => {
|
||||
match dispatch(
|
||||
server,
|
||||
|
|
@ -118,16 +312,16 @@ fn run_repl<R: BufRead, W: Write>(
|
|||
Ok(Reply::TickCompleted { ticks }) => {
|
||||
writeln!(output, "ran {ticks} scheduler tick(s)")
|
||||
}
|
||||
Ok(other) => unexpected_reply(&mut output, other),
|
||||
Ok(other) => unexpected_reply(output, other),
|
||||
Err(error) => writeln!(output, "error: {error}"),
|
||||
}
|
||||
}
|
||||
Err(error) => writeln!(output, "error: {error}"),
|
||||
},
|
||||
"inspect" => match parse_exact::<u64, 1>(&args) {
|
||||
Ok([id]) => match dispatch(server, Command::InspectActor { actor_id: id }) {
|
||||
"inspect" => match parse_actor_argument(args, state) {
|
||||
Ok(actor_ref) => match dispatch(server, Command::InspectActor { actor_ref }) {
|
||||
Ok(Reply::Actor(actor)) => writeln!(output, "{actor:#?}"),
|
||||
Ok(other) => unexpected_reply(&mut output, other),
|
||||
Ok(other) => unexpected_reply(output, other),
|
||||
Err(error) => writeln!(output, "error: {error}"),
|
||||
},
|
||||
Err(error) => writeln!(output, "error: {error}"),
|
||||
|
|
@ -139,42 +333,42 @@ fn run_repl<R: BufRead, W: Write>(
|
|||
}
|
||||
Ok(())
|
||||
}
|
||||
Ok(other) => unexpected_reply(&mut output, other),
|
||||
Ok(other) => unexpected_reply(output, other),
|
||||
Err(error) => writeln!(output, "error: {error}"),
|
||||
},
|
||||
"events" => writeln!(output, "error: usage: events"),
|
||||
"stop" => lifecycle_command(&mut output, &args, server, |actor_id| {
|
||||
Command::StopActor { actor_id }
|
||||
"stop" => lifecycle_command(output, args, server, state, |actor_ref| {
|
||||
Command::StopActor { actor_ref }
|
||||
}),
|
||||
"restart" => lifecycle_command(&mut output, &args, server, |actor_id| {
|
||||
Command::RestartActor { actor_id }
|
||||
"restart" => lifecycle_command(output, args, server, state, |actor_ref| {
|
||||
Command::RestartActor { actor_ref }
|
||||
}),
|
||||
"pause" => lifecycle_command(&mut output, &args, server, |actor_id| {
|
||||
Command::PauseActor { actor_id }
|
||||
"pause" => lifecycle_command(output, args, server, state, |actor_ref| {
|
||||
Command::PauseActor { actor_ref }
|
||||
}),
|
||||
"resume" => lifecycle_command(&mut output, &args, server, |actor_id| {
|
||||
Command::ResumeActor { actor_id }
|
||||
"resume" => lifecycle_command(output, args, server, state, |actor_ref| {
|
||||
Command::ResumeActor { actor_ref }
|
||||
}),
|
||||
"cancel" => lifecycle_command(&mut output, &args, server, |actor_id| {
|
||||
Command::CancelActor { actor_id }
|
||||
"cancel" => lifecycle_command(output, args, server, state, |actor_ref| {
|
||||
Command::CancelActor { actor_ref }
|
||||
}),
|
||||
"quit" | "exit" if args.is_empty() => break,
|
||||
"quit" | "exit" if args.is_empty() => return Ok(false),
|
||||
"quit" | "exit" => writeln!(output, "error: usage: quit"),
|
||||
_ => writeln!(output, "error: unknown command `{command}`; type `help`"),
|
||||
};
|
||||
result?;
|
||||
}
|
||||
Ok(())
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn lifecycle_command<W: Write>(
|
||||
output: &mut W,
|
||||
args: &[&str],
|
||||
server: &mut Server,
|
||||
operation: impl FnOnce(u64) -> Command,
|
||||
state: &ReplState,
|
||||
operation: impl FnOnce(String) -> Command,
|
||||
) -> io::Result<()> {
|
||||
match parse_exact::<u64, 1>(args) {
|
||||
Ok([id]) => match dispatch(server, operation(id)) {
|
||||
match parse_actor_argument(args, state) {
|
||||
Ok(actor_ref) => match dispatch(server, operation(actor_ref)) {
|
||||
Ok(Reply::Acknowledged) => writeln!(output, "ok"),
|
||||
Ok(other) => unexpected_reply(output, other),
|
||||
Err(error) => writeln!(output, "error: {error}"),
|
||||
|
|
@ -183,54 +377,64 @@ fn lifecycle_command<W: Write>(
|
|||
}
|
||||
}
|
||||
|
||||
fn spawn_command<W: Write>(
|
||||
output: &mut W,
|
||||
args: &[&str],
|
||||
default_behavior: &str,
|
||||
server: &mut Server,
|
||||
) -> io::Result<()> {
|
||||
let (state_arg, behavior_name) = match args {
|
||||
[state] => (*state, default_behavior),
|
||||
[state, behavior_name] => (*state, *behavior_name),
|
||||
_ => {
|
||||
fn spawn_command<W: Write>(output: &mut W, args: &[&str], server: &mut Server) -> io::Result<()> {
|
||||
let [behavior_name, initial_state] = args else {
|
||||
return writeln!(
|
||||
output,
|
||||
"error: usage: spawn <initial-state> [behavior-name]"
|
||||
"error: usage: spawn <behavior-name> <initial-state>"
|
||||
);
|
||||
};
|
||||
match spawn_actor(behavior_name, initial_state, server) {
|
||||
Ok(actor_ref) => writeln!(output, "spawned @{actor_ref} from `{behavior_name}`"),
|
||||
Err(error) => writeln!(output, "error: {error}"),
|
||||
}
|
||||
};
|
||||
let state = match state_arg.parse() {
|
||||
Ok(state) => state,
|
||||
Err(_) => return writeln!(output, "error: invalid initial state: `{state_arg}`"),
|
||||
};
|
||||
let response = dispatch(
|
||||
}
|
||||
|
||||
fn spawn_actor(
|
||||
behavior_name: &str,
|
||||
initial_state: &str,
|
||||
server: &mut Server,
|
||||
) -> Result<String, String> {
|
||||
let state = initial_state
|
||||
.parse()
|
||||
.map_err(|_| format!("invalid initial state: `{initial_state}`"))?;
|
||||
match dispatch(
|
||||
server,
|
||||
Command::Spawn {
|
||||
behavior_name: behavior_name.to_owned(),
|
||||
state,
|
||||
mailbox_capacity: MAILBOX_CAPACITY,
|
||||
},
|
||||
);
|
||||
match response {
|
||||
Ok(Reply::ActorSpawned { actor_id }) => {
|
||||
let revision = match dispatch(
|
||||
server,
|
||||
Command::GetBehavior {
|
||||
name: behavior_name.to_owned(),
|
||||
},
|
||||
) {
|
||||
Ok(Reply::Behavior(behavior)) => behavior.revision,
|
||||
Ok(other) => return unexpected_reply(output, other),
|
||||
Err(error) => return writeln!(output, "error: {error}"),
|
||||
)? {
|
||||
Reply::ActorSpawned { actor_ref } => Ok(actor_ref),
|
||||
other => Err(format!("unexpected protocol response: {other:?}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_actor_argument(args: &[&str], state: &ReplState) -> Result<String, String> {
|
||||
let [argument] = args else {
|
||||
return Err(format!("expected 1 actor reference, got {}", args.len()));
|
||||
};
|
||||
writeln!(
|
||||
output,
|
||||
"spawned ActorId({actor_id}) with `{behavior_name}` revision {revision}"
|
||||
)
|
||||
if let Some(actor_ref) = state.bindings.get(*argument) {
|
||||
return Ok(actor_ref.clone());
|
||||
}
|
||||
Ok(other) => unexpected_reply(output, other),
|
||||
Err(error) => writeln!(output, "error: {error}"),
|
||||
let actor_ref = argument.strip_prefix('@').unwrap_or(argument);
|
||||
if actor_ref.len() != 21
|
||||
|| !actor_ref
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
|
||||
{
|
||||
return Err(format!(
|
||||
"unknown actor binding or invalid actor reference `{argument}`"
|
||||
));
|
||||
}
|
||||
Ok(actor_ref.to_owned())
|
||||
}
|
||||
|
||||
fn is_binding_name(name: &str) -> bool {
|
||||
let mut characters = name.chars();
|
||||
matches!(characters.next(), Some(character) if character == '_' || character.is_ascii_alphabetic())
|
||||
&& characters.all(|character| character == '_' || character.is_ascii_alphanumeric())
|
||||
}
|
||||
|
||||
fn publish_command<W: Write>(output: &mut W, args: &[&str], server: &mut Server) -> io::Result<()> {
|
||||
|
|
@ -339,18 +543,26 @@ fn rollback_command<W: Write>(
|
|||
}
|
||||
}
|
||||
|
||||
fn install_command<W: Write>(output: &mut W, args: &[&str], server: &mut Server) -> io::Result<()> {
|
||||
let [actor_id, name] = args else {
|
||||
return writeln!(output, "error: usage: install <actor-id> <behavior-name>");
|
||||
fn install_command<W: Write>(
|
||||
output: &mut W,
|
||||
args: &[&str],
|
||||
server: &mut Server,
|
||||
state: &ReplState,
|
||||
) -> io::Result<()> {
|
||||
let [actor_arg, name] = args else {
|
||||
return writeln!(
|
||||
output,
|
||||
"error: usage: install <actor-ref-or-name> <behavior-name>"
|
||||
);
|
||||
};
|
||||
let actor_id = match actor_id.parse::<u64>() {
|
||||
Ok(id) => id,
|
||||
Err(_) => return writeln!(output, "error: invalid actor ID"),
|
||||
let actor_ref = match parse_actor_argument(&[*actor_arg], state) {
|
||||
Ok(actor_ref) => actor_ref,
|
||||
Err(error) => return writeln!(output, "error: {error}"),
|
||||
};
|
||||
let implementation = match dispatch(
|
||||
server,
|
||||
Command::InstallBehavior {
|
||||
actor_id,
|
||||
actor_ref: actor_ref.clone(),
|
||||
behavior_name: (*name).to_owned(),
|
||||
},
|
||||
) {
|
||||
|
|
@ -367,8 +579,8 @@ fn install_command<W: Write>(output: &mut W, args: &[&str], server: &mut Server)
|
|||
Ok(Reply::Behavior(behavior)) => {
|
||||
writeln!(
|
||||
output,
|
||||
"installed `{}` revision {} on actor {} ({implementation})",
|
||||
behavior.name, behavior.revision, actor_id
|
||||
"installed `{}` revision {} on actor @{} ({implementation})",
|
||||
behavior.name, behavior.revision, actor_ref
|
||||
)
|
||||
}
|
||||
Ok(other) => unexpected_reply(output, other),
|
||||
|
|
@ -389,23 +601,6 @@ fn unexpected_reply<W: Write>(output: &mut W, reply: Reply) -> io::Result<()> {
|
|||
writeln!(output, "error: unexpected protocol response: {reply:?}")
|
||||
}
|
||||
|
||||
fn parse_exact<T: std::str::FromStr, const N: usize>(args: &[&str]) -> Result<[T; N], String> {
|
||||
if args.len() != N {
|
||||
return Err(format!("expected {N} argument(s), got {}", args.len()));
|
||||
}
|
||||
let parsed = args
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, arg)| {
|
||||
arg.parse::<T>()
|
||||
.map_err(|_| format!("invalid argument {}: `{arg}`", index + 1))
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
parsed
|
||||
.try_into()
|
||||
.map_err(|_| "invalid argument count".to_owned())
|
||||
}
|
||||
|
||||
fn parse_tick_args(args: &[&str]) -> Result<(usize, NonZeroUsize), String> {
|
||||
match args {
|
||||
[count] => Ok((
|
||||
|
|
@ -428,42 +623,49 @@ fn parse_tick_args(args: &[&str]) -> Result<(usize, NonZeroUsize), String> {
|
|||
}
|
||||
}
|
||||
|
||||
fn parse_send_args(args: &[&str]) -> Result<(u64, i64, i64), String> {
|
||||
if args.len() != 3 {
|
||||
return Err(format!("expected 3 arguments, got {}", args.len()));
|
||||
}
|
||||
let actor = args[0]
|
||||
fn parse_send_args(
|
||||
args: &[&str],
|
||||
state: &ReplState,
|
||||
) -> Result<(String, i64, i64, Option<String>), String> {
|
||||
let (actor_arg, kind_arg, value_arg, target_arg) = match args {
|
||||
[actor, kind, value] => (*actor, *kind, *value, None),
|
||||
[actor, kind, value, "--to", target] => (*actor, *kind, *value, Some(*target)),
|
||||
_ => return Err("usage: send <actor> <kind> <value> [--to <target>]".to_owned()),
|
||||
};
|
||||
let actor_ref = parse_actor_argument(&[actor_arg], state)?;
|
||||
let kind = kind_arg
|
||||
.parse()
|
||||
.map_err(|_| format!("invalid actor ID: `{}`", args[0]))?;
|
||||
let kind = args[1]
|
||||
.map_err(|_| format!("invalid message kind: `{kind_arg}`"))?;
|
||||
let value = value_arg
|
||||
.parse()
|
||||
.map_err(|_| format!("invalid message kind: `{}`", args[1]))?;
|
||||
let value = args[2]
|
||||
.parse()
|
||||
.map_err(|_| format!("invalid message value: `{}`", args[2]))?;
|
||||
Ok((actor, kind, value))
|
||||
.map_err(|_| format!("invalid message value: `{value_arg}`"))?;
|
||||
let target_ref = target_arg
|
||||
.map(|target| parse_actor_argument(&[target], state))
|
||||
.transpose()?;
|
||||
Ok((actor_ref, kind, value, target_ref))
|
||||
}
|
||||
|
||||
fn print_help<W: Write>(output: &mut W) -> io::Result<()> {
|
||||
writeln!(
|
||||
output,
|
||||
"Commands:\n\
|
||||
spawn <initial-state> [behavior-name]\n\
|
||||
let <name> = spawn <behavior-name> <initial-state>\n\
|
||||
spawn <behavior-name> <initial-state>\n\
|
||||
behaviors\n\
|
||||
publish <behavior-name> <new|expected-revision> <source-file>\n\
|
||||
history <behavior-name>\n\
|
||||
rollback <behavior-name> <expected-revision> <target-revision>\n\
|
||||
install <actor-id> <behavior-name>\n\
|
||||
send <actor-id> <kind> <value>\n\
|
||||
install <actor-ref-or-name> <behavior-name>\n\
|
||||
send <actor-ref-or-name> <kind> <value> [--to <actor-ref-or-name>]\n\
|
||||
tick <count> [instruction-budget]\n\
|
||||
inspect <actor-id>\n\
|
||||
inspect <actor-ref-or-name>\n\
|
||||
actors\n\
|
||||
events\n\
|
||||
stop <actor-id>\n\
|
||||
restart <actor-id>\n\
|
||||
pause <actor-id>\n\
|
||||
resume <actor-id>\n\
|
||||
cancel <actor-id>\n\
|
||||
stop <actor-ref-or-name>\n\
|
||||
restart <actor-ref-or-name>\n\
|
||||
pause <actor-ref-or-name>\n\
|
||||
resume <actor-ref-or-name>\n\
|
||||
cancel <actor-ref-or-name>\n\
|
||||
help\n\
|
||||
quit"
|
||||
)
|
||||
|
|
@ -478,7 +680,7 @@ mod tests {
|
|||
dispatch(
|
||||
&mut server,
|
||||
Command::PublishBehavior {
|
||||
name: DEFAULT_BEHAVIOR.to_owned(),
|
||||
name: COUNTER_BEHAVIOR.to_owned(),
|
||||
expected_revision: None,
|
||||
source: include_str!("../../../runa-std/actors/counter.rn").to_owned(),
|
||||
},
|
||||
|
|
@ -491,26 +693,29 @@ mod tests {
|
|||
fn repl_drives_actor_messages_and_inspection() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut server = server();
|
||||
let input = io::Cursor::new(
|
||||
"spawn 0\nspawn 10\nsend 1 1 2\ntick 100\ninspect 1\ninspect 2\nevents\nquit\n",
|
||||
"let sender = spawn counter 0\nlet receiver = spawn counter 10\nsend sender 1 5 --to receiver\ntick 100\ninspect sender\ninspect receiver\nevents\nquit\n",
|
||||
);
|
||||
let mut output = Vec::new();
|
||||
run_repl(input, &mut output, &mut server, DEFAULT_BEHAVIOR)?;
|
||||
run_repl(input, &mut output, &mut server)?;
|
||||
let output = String::from_utf8(output)?;
|
||||
assert!(output.contains("actor_id: 1"));
|
||||
assert!(output.contains("actor_id: 2"));
|
||||
assert!(output.contains("sender = @"));
|
||||
assert!(output.contains("receiver = @"));
|
||||
assert!(output.contains("state: 15"));
|
||||
assert!(output.contains("Committed {"));
|
||||
assert!(output.contains("actor_id: 2"));
|
||||
assert!(output.contains("actor_ref: \""));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repl_reports_bad_input_and_continues() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut server = server();
|
||||
let input = io::Cursor::new("spawn nope\nsend 999 0 1\ntick 1 0\nunknown\nquit\n");
|
||||
let input = io::Cursor::new(
|
||||
"spawn 0\nspawn counter nope\nsend 000000000000000000000 0 1\ntick 1 0\nunknown\nquit\n",
|
||||
);
|
||||
let mut output = Vec::new();
|
||||
run_repl(input, &mut output, &mut server, DEFAULT_BEHAVIOR)?;
|
||||
run_repl(input, &mut output, &mut server)?;
|
||||
let output = String::from_utf8(output)?;
|
||||
assert!(output.contains("usage: spawn <behavior-name> <initial-state>"));
|
||||
assert!(output.contains("invalid initial state"));
|
||||
assert!(output.contains("unknown actor"));
|
||||
assert!(output.contains("tick budget must be positive"));
|
||||
|
|
@ -527,35 +732,51 @@ mod tests {
|
|||
"/../../runa-std/actors/counter.rn"
|
||||
);
|
||||
let input = io::Cursor::new(format!(
|
||||
"behaviors\npublish counter 1 {source_path}\nhistory counter\nrollback counter 2 1\nspawn 0 counter\ninstall 1 counter\nsend 1 0 1\ntick 1 1\ncancel 1\npause 1\nresume 1\ninspect 1\nevents\nquit\n"
|
||||
"behaviors\npublish counter 1 {source_path}\nhistory counter\nrollback counter 2 1\nlet actor = spawn counter 0\ninstall actor counter\nsend actor 0 1\ntick 1 1\ncancel actor\npause actor\nresume actor\ninspect actor\nevents\nquit\n"
|
||||
));
|
||||
let mut output = Vec::new();
|
||||
run_repl(input, &mut output, &mut server, DEFAULT_BEHAVIOR)?;
|
||||
run_repl(input, &mut output, &mut server)?;
|
||||
let output = String::from_utf8(output)?;
|
||||
assert!(output.contains("published `counter` revision 2"));
|
||||
assert!(output.contains("revision 1:"));
|
||||
assert!(output.contains("revision 2:"));
|
||||
assert!(output.contains("rolled back `counter`"));
|
||||
assert!(output.contains("spawned ActorId(1) with `counter` revision 3"));
|
||||
assert!(output.contains("installed `counter` revision 3 on actor 1"));
|
||||
assert!(output.contains("actor = @"));
|
||||
assert!(output.contains("installed `counter` revision 3 on actor @"));
|
||||
assert!(output.contains("Paused {"));
|
||||
assert!(output.contains("Resumed {"));
|
||||
assert!(output.contains("InvocationCancelled {"));
|
||||
let behavior = match dispatch(
|
||||
&mut server,
|
||||
Command::GetBehavior {
|
||||
name: DEFAULT_BEHAVIOR.to_owned(),
|
||||
name: COUNTER_BEHAVIOR.to_owned(),
|
||||
},
|
||||
)? {
|
||||
Reply::Behavior(behavior) => behavior,
|
||||
other => return Err(format!("unexpected response: {other:?}").into()),
|
||||
};
|
||||
assert_eq!(behavior.revision, 3);
|
||||
let actor = match dispatch(&mut server, Command::InspectActor { actor_id: 1 })? {
|
||||
let actor_ref = match dispatch(&mut server, Command::ListActors)? {
|
||||
Reply::Actors(actors) => actors[0].actor_ref.clone(),
|
||||
other => return Err(format!("unexpected response: {other:?}").into()),
|
||||
};
|
||||
let actor = match dispatch(&mut server, Command::InspectActor { actor_ref })? {
|
||||
Reply::Actor(actor) => actor,
|
||||
other => return Err(format!("unexpected response: {other:?}").into()),
|
||||
};
|
||||
assert_eq!(actor.implementation_id, behavior.current_implementation_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completion_offers_commands_behaviors_and_named_actor_refs() {
|
||||
let mut helper = ReplHelper::default();
|
||||
let mut bindings = BTreeMap::new();
|
||||
bindings.insert("sender".to_owned(), "xK8_pQ2-Lm5_012345678".to_owned());
|
||||
helper.set_context(vec!["counter".to_owned()], &bindings);
|
||||
|
||||
assert_eq!(helper.candidates("sp", 2), vec!["spawn"]);
|
||||
assert_eq!(helper.candidates("spawn co", 8), vec!["counter"]);
|
||||
assert_eq!(helper.candidates("inspect se", 10), vec!["sender"]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,19 +71,25 @@ An actor comprises distinct components:
|
|||
|
||||
```text
|
||||
Actor {
|
||||
id: ActorId, // stable logical identity
|
||||
actor_ref: ActorRef, // opaque public reference
|
||||
mailbox: MailboxId, // independent queue; may survive restart
|
||||
state: StateRecord, // committed private state
|
||||
behavior: ImplementationId, // immutable handler implementation
|
||||
incarnation: Generation, // changes on restart
|
||||
status: ActorStatus,
|
||||
supervisor: Option<ActorId>,
|
||||
supervisor: Option<ActorRef>,
|
||||
}
|
||||
```
|
||||
|
||||
### 4.1 Identity
|
||||
|
||||
Actor IDs are opaque, never raw pointers. Use generation or other safeguards against stale references. A logical actor may be restarted with the same ID and mailbox but a new incarnation. Distinguish runtime-instance identity from actor identity if remote or persisted references are introduced.
|
||||
Actor references are opaque, never raw pointers or sequential numbers. The
|
||||
prototype uses 21-character URL-safe NanoIDs for public references. A logical
|
||||
actor retains its reference and mailbox across restart but receives a new
|
||||
incarnation. Random references are not an authentication or authorization
|
||||
mechanism; network access remains loopback-only until access control exists.
|
||||
Distinguish runtime-instance identity from actor identity if remote or
|
||||
persisted references are introduced.
|
||||
|
||||
### 4.2 Mailbox
|
||||
|
||||
|
|
@ -304,7 +310,7 @@ The diagram is conceptual; Cargo dependencies should be kept minimal. A dependen
|
|||
|
||||
| Crate | Owns | May depend on |
|
||||
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
|
||||
| `runa-core` | Small, stable value types shared across independent layers: actor IDs, source and implementation IDs, canonical message value types, common error identifiers | Minimal external libraries only |
|
||||
| `runa-core` | Small, stable value types shared across independent layers: source and implementation IDs, canonical message value types, common error identifiers | Minimal external libraries only |
|
||||
| `runa-code` | Original source retention, versioned source/implementation hashing, compile-input fingerprints, in-memory compiled artifact cache | `runa-core`, `runa-rune`, BLAKE3 |
|
||||
| `runa-actor` | Mailbox and state-transition semantics, message reservation, acknowledgment, effects, actor lifecycle definitions | `runa-core` |
|
||||
| `runa-rune` | Rune compilation, VM execution, suspension, interruption hooks, language-specific diagnostics | `runa-core`, Rune libraries |
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ when started with `--listen`, over TCP bound to an IPv4 or IPv6 loopback
|
|||
address. The TCP listener is unauthenticated and is intended only for trusted
|
||||
local development.
|
||||
|
||||
## Version 1
|
||||
## Version 2
|
||||
|
||||
Every request and response carries `version: 1`. Requests also carry a
|
||||
Every request and response carries `version: 2`. Requests also carry a
|
||||
non-empty `request_id` used to correlate the response. A server must reject
|
||||
unsupported versions with `unsupported_version` rather than interpreting a
|
||||
request using a different contract. Events carry the protocol version and do
|
||||
|
|
@ -19,19 +19,23 @@ Requests have a tagged command object:
|
|||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"version": 2,
|
||||
"request_id": "req-1",
|
||||
"command": {
|
||||
"operation": "send",
|
||||
"parameters": {
|
||||
"actor_id": 7,
|
||||
"message": { "kind": 2, "value": 9 }
|
||||
"actor_ref": "xK8_pQ2-Lm5_012345678",
|
||||
"message": {
|
||||
"kind": 2,
|
||||
"value": 9,
|
||||
"target_ref": "Hj2_9sM0-pQ4_7aBcD3eF"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The version 1 operations are behavior publication, rollback, lookup, source
|
||||
The version 2 operations are behavior publication, rollback, lookup, source
|
||||
retrieval, actor spawn, send, tick, inspect, actor listing, event retrieval,
|
||||
stop, restart, pause, resume, cancellation, and behavior installation.
|
||||
Migration closures are intentionally excluded: they are host callbacks, not
|
||||
|
|
@ -40,7 +44,9 @@ portable wire values.
|
|||
Responses echo the request ID and carry either a typed reply or a protocol
|
||||
error. Events use a separate versioned envelope. Actor and behavior snapshots
|
||||
are wire DTOs, not serialized runtime objects; implementation identities are
|
||||
represented by their stable display strings. The server maps known service
|
||||
represented by their stable display strings. Public actor identity is a
|
||||
21-character URL-safe NanoID string carried in `actor_ref`; message `target_ref`
|
||||
is optional and omitted when absent. The server maps known service
|
||||
failures to the defined error codes and otherwise uses `runtime_failure`
|
||||
rather than claiming a more specific category.
|
||||
|
||||
|
|
@ -58,8 +64,8 @@ must not be enabled until appropriate authentication and authorization exist.
|
|||
The `runa-client` crate launches the stdio server locally and validates
|
||||
response protocol versions and request IDs.
|
||||
|
||||
The current validation checks the protocol version, request ID, positive
|
||||
actor/revision identifiers where required, non-empty behavior names, mailbox
|
||||
The current validation checks the protocol version, request ID, actor-reference
|
||||
shape, positive revisions where required, non-empty behavior names, mailbox
|
||||
capacity, and instruction budget. Behavior-name grammar, actor existence,
|
||||
current behavior revisions, mailbox capacity at send time, and all runtime
|
||||
invariants remain authoritative at the application-service boundary.
|
||||
|
|
|
|||
|
|
@ -6,41 +6,44 @@ Start the interactive, in-memory runtime session with:
|
|||
mise run run
|
||||
```
|
||||
|
||||
The executable loads [counter.rn](../runa-std/actors/counter.rn) as the default
|
||||
behavior. Commands include `spawn`, `send`, `tick`, `inspect`, `actors`,
|
||||
The executable publishes [counter.rn](../runa-std/actors/counter.rn) as the
|
||||
`counter` behavior. Commands include `spawn`, `send`, `tick`, `inspect`, `actors`,
|
||||
`events`, `stop`, `restart`, `pause`, `resume`, `cancel`, `behaviors`,
|
||||
`publish`, `history`, `rollback`, and `install`; enter `help` to see usage.
|
||||
`publish` reads a Rune source file and takes `new` for a first revision or the
|
||||
expected current revision for an update. `rollback` requires the expected
|
||||
current revision and a target historical revision. `install` selects a named
|
||||
behavior's current implementation and fails if the actor is not at an idle
|
||||
handler boundary.
|
||||
`spawn` accepts an optional behavior name and otherwise uses `counter`. All
|
||||
commands operate on the same process-local runtime and code store. This CLI is
|
||||
for local development, not a versioned external API.
|
||||
handler boundary. `spawn` requires a behavior name and initial state, in that
|
||||
order. All commands operate on the same process-local runtime and code store.
|
||||
The CLI uses the versioned protocol dispatcher, also available over stdio or
|
||||
the loopback-only TCP listener for local development.
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
runa> spawn 0
|
||||
spawned ActorId(1)
|
||||
runa> spawn 10
|
||||
spawned ActorId(2)
|
||||
runa> send 1 1 2
|
||||
runa> let sender = spawn counter 0
|
||||
sender = @<actor-reference>
|
||||
runa> let receiver = spawn counter 10
|
||||
receiver = @<actor-reference>
|
||||
runa> send sender 1 5 --to receiver
|
||||
message queued
|
||||
runa> tick 100
|
||||
runa> inspect 1
|
||||
runa> inspect 2
|
||||
runa> inspect sender
|
||||
runa> inspect receiver
|
||||
```
|
||||
|
||||
The sender commits state 1 and sends a structured message; the receiver commits
|
||||
state 15. `tick <count> [instruction-budget]` explicitly advances the scheduler.
|
||||
The default per-actor slice budget is 32 instructions.
|
||||
state 15. Actor references are 21-character NanoIDs; names are local REPL
|
||||
bindings. Tab completes commands, behaviors, and bound actor references, while
|
||||
Up/Down navigate command history. `tick <count> [instruction-budget]`
|
||||
explicitly advances the scheduler. The default per-actor slice budget is 32
|
||||
instructions.
|
||||
|
||||
## Current contracts
|
||||
|
||||
- `runa-actor` owns actor, mailbox and implementation IDs, structured messages,
|
||||
snapshots, statuses and lifecycle events.
|
||||
- `runa-actor` owns actor references, mailbox and implementation IDs, structured
|
||||
messages, snapshots, statuses and lifecycle events.
|
||||
- `runa-core` owns stable source and implementation identity types.
|
||||
- `runa-code` preserves original source, derives identities, caches compiled
|
||||
artifacts, and provides an in-memory named behavior registry with revisions.
|
||||
|
|
@ -52,8 +55,8 @@ The default per-actor slice budget is 32 instructions.
|
|||
- `runa-protocol` defines versioned transport-neutral request, response, and
|
||||
event DTOs; it does not dispatch requests or access runtime state.
|
||||
- `runa-server` dispatches those requests through `runa-image-api` and maps
|
||||
results to protocol replies; its executable uses newline-delimited JSON over
|
||||
stdin/stdout with a 1 MiB request-line limit, not a network listener.
|
||||
results to protocol replies; its executable accepts newline-delimited JSON
|
||||
over stdin/stdout or TCP bound to loopback, with a 1 MiB request-line limit.
|
||||
- `runa` provides an interactive CLI client using protocol requests through the
|
||||
dispatcher.
|
||||
|
||||
|
|
@ -66,8 +69,8 @@ published source for a selected behavior revision.
|
|||
|
||||
An implementation is published once under an ID. Actors reference that exact
|
||||
implementation. Implementation IDs use source content and the compile
|
||||
fingerprint; actor and mailbox IDs remain process-local counters. Actor IDs and
|
||||
incarnation counters use checked increments.
|
||||
fingerprint; actor references are random NanoIDs and mailbox IDs remain
|
||||
process-local counters. Incarnation counters use checked increments.
|
||||
|
||||
`CodeStore::publish_behavior` compiles source before appending a behavior
|
||||
revision. Creation expects no existing revision; updates require the caller's
|
||||
|
|
@ -133,19 +136,26 @@ callers must change it when registered native functions or their contracts
|
|||
change. The CLI currently creates a fresh `CodeStore` when it starts.
|
||||
|
||||
Prototype state is an `i64`; messages contain integer `kind` and `value`
|
||||
fields. Rune handlers have this deliberately restricted interface:
|
||||
fields, plus an optional target actor reference for a handler-produced send.
|
||||
Public actor references are 21-character URL-safe NanoIDs. They are generated
|
||||
when actors are spawned and remain stable across stop/restart; mailbox IDs are
|
||||
separate internal counters. Rune handlers have this deliberately restricted
|
||||
interface:
|
||||
|
||||
```text
|
||||
handle(state, kind, value) -> (new_state, [(target_actor_id, kind, value), ...])
|
||||
handle(state, kind, value, target_ref)
|
||||
-> (new_state, [(target_ref, kind, value), ...])
|
||||
```
|
||||
|
||||
`target_ref` is an empty string when the incoming message has no routed target.
|
||||
The returned effect vector describes sends; handlers cannot access the runtime
|
||||
or directly mutate committed state. General schema/versioned values and
|
||||
capability-bearing contexts are future work.
|
||||
|
||||
## Scheduling and lifecycle
|
||||
|
||||
`tick(nonzero_budget)` gives each runnable actor one bounded slice in ID order.
|
||||
`tick(nonzero_budget)` gives each runnable actor one bounded slice in actor
|
||||
reference order.
|
||||
FIFO queues are bounded, including the reserved message in capacity accounting.
|
||||
A queued message is reserved when its invocation starts. Subsequent messages
|
||||
stay queued, including while the actor awaits. A wake marks an awaiting actor
|
||||
|
|
@ -166,9 +176,10 @@ silently acknowledged or automatically retried. External sends return errors
|
|||
for unknown, unavailable, or full actors.
|
||||
|
||||
Stopping drops the invocation, retaining committed state, mailbox and reserved
|
||||
message. Restarting a stopped or failed actor preserves its actor ID, mailbox
|
||||
ID, implementation ID, committed state, and queued-message order; it increments
|
||||
its incarnation and puts the reserved message at the front of the queue.
|
||||
message. Restarting a stopped or failed actor preserves its actor reference,
|
||||
mailbox ID, implementation ID, committed state, and queued-message order; it
|
||||
increments its incarnation and puts the reserved message at the front of the
|
||||
queue.
|
||||
Restart is explicit. Repeated failure of the same reserved message can therefore
|
||||
block later messages until an operator changes the implementation or otherwise
|
||||
resolves it; there is no automatic poison-message policy or supervision.
|
||||
|
|
@ -208,11 +219,10 @@ capacity, and successful message effects.
|
|||
## Limits
|
||||
|
||||
This is not yet a full image: no persistent behavior registry or mailboxes,
|
||||
transactional outbox, supervisors, API server, GUI, or automatic image
|
||||
bootstrap. The behavior registry and implementation installation are
|
||||
process-local; installation requires an idle handler boundary and does not
|
||||
upgrade active execution. The example default script is bundled through
|
||||
`include_str!`.
|
||||
transactional outbox, supervisors, GUI, or automatic image bootstrap. The
|
||||
behavior registry and implementation installation are process-local;
|
||||
installation requires an idle handler boundary and does not upgrade active
|
||||
execution. The example counter script is bundled through `include_str!`.
|
||||
Messages are not accepted by stopped/failed actors.
|
||||
|
||||
Native functions must be nonblocking. Dropping a future proves cleanup only
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
pub fn handle(state, kind, value) {
|
||||
pub fn handle(state, kind, value, target_ref) {
|
||||
if kind == 1 {
|
||||
(state + 1, [(value, 0, 5)])
|
||||
(state + 1, [(target_ref, 0, 5)])
|
||||
} else {
|
||||
(state + value, [])
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue