├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── benches ├── mailbox.rs └── workload.rs ├── doc ├── design-guide.md └── log-and-mon.md ├── examples ├── fail.rs ├── full.rs ├── gossip.rs ├── hello.rs ├── paxos.rs ├── pi.rs ├── ping.rs ├── remote.rs └── stop.rs ├── src ├── api.rs ├── config.rs ├── core.rs ├── error.rs ├── lib.rs ├── monitor.rs ├── pool.rs └── remote │ ├── client.rs │ ├── ip.rs │ ├── mod.rs │ ├── packet.rs │ └── server.rs └── tests ├── actors.rs └── remote.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ develop, master ] 6 | pull_request: 7 | branches: [ develop ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-22.04 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Check 15 | run: cargo check --all-features --examples --tests --benches 16 | - name: Build 17 | run: cargo build --all-features 18 | - name: Run tests 19 | run: cargo test --all-features 20 | - name: Run benches 21 | run: cargo bench --all-features 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | 3 | .idea/ 4 | 5 | *.log 6 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.7.19" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "ansi_term" 16 | version = "0.12.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" 19 | dependencies = [ 20 | "winapi 0.3.9", 21 | ] 22 | 23 | [[package]] 24 | name = "atty" 25 | version = "0.2.14" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 28 | dependencies = [ 29 | "hermit-abi", 30 | "libc", 31 | "winapi 0.3.9", 32 | ] 33 | 34 | [[package]] 35 | name = "bencher" 36 | version = "0.1.5" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "7dfdb4953a096c551ce9ace855a604d702e6e62d77fac690575ae347571717f5" 39 | 40 | [[package]] 41 | name = "bitflags" 42 | version = "1.3.2" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 45 | 46 | [[package]] 47 | name = "bytes" 48 | version = "1.2.1" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" 51 | 52 | [[package]] 53 | name = "c_linked_list" 54 | version = "1.1.1" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "4964518bd3b4a8190e832886cdc0da9794f12e8e6c1613a9e90ff331c4c8724b" 57 | 58 | [[package]] 59 | name = "cfg-if" 60 | version = "1.0.0" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 63 | 64 | [[package]] 65 | name = "clap" 66 | version = "2.34.0" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" 69 | dependencies = [ 70 | "ansi_term", 71 | "atty", 72 | "bitflags", 73 | "strsim", 74 | "textwrap", 75 | "unicode-width", 76 | "vec_map", 77 | ] 78 | 79 | [[package]] 80 | name = "core_affinity" 81 | version = "0.5.10" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "7f8a03115cc34fb0d7c321dd154a3914b3ca082ccc5c11d91bf7117dbbe7171f" 84 | dependencies = [ 85 | "kernel32-sys", 86 | "libc", 87 | "num_cpus", 88 | "winapi 0.2.8", 89 | ] 90 | 91 | [[package]] 92 | name = "crossbeam-channel" 93 | version = "0.5.6" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" 96 | dependencies = [ 97 | "cfg-if", 98 | "crossbeam-utils", 99 | ] 100 | 101 | [[package]] 102 | name = "crossbeam-utils" 103 | version = "0.8.11" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc" 106 | dependencies = [ 107 | "cfg-if", 108 | "once_cell", 109 | ] 110 | 111 | [[package]] 112 | name = "env_logger" 113 | version = "0.8.4" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" 116 | dependencies = [ 117 | "atty", 118 | "humantime", 119 | "log", 120 | "regex", 121 | "termcolor", 122 | ] 123 | 124 | [[package]] 125 | name = "gcc" 126 | version = "0.3.55" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" 129 | 130 | [[package]] 131 | name = "get_if_addrs" 132 | version = "0.5.3" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "abddb55a898d32925f3148bd281174a68eeb68bbfd9a5938a57b18f506ee4ef7" 135 | dependencies = [ 136 | "c_linked_list", 137 | "get_if_addrs-sys", 138 | "libc", 139 | "winapi 0.2.8", 140 | ] 141 | 142 | [[package]] 143 | name = "get_if_addrs-sys" 144 | version = "0.1.1" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "0d04f9fb746cf36b191c00f3ede8bde9c8e64f9f4b05ae2694a9ccf5e3f5ab48" 147 | dependencies = [ 148 | "gcc", 149 | "libc", 150 | ] 151 | 152 | [[package]] 153 | name = "getrandom" 154 | version = "0.1.16" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" 157 | dependencies = [ 158 | "cfg-if", 159 | "libc", 160 | "wasi 0.9.0+wasi-snapshot-preview1", 161 | ] 162 | 163 | [[package]] 164 | name = "hermit-abi" 165 | version = "0.1.19" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 168 | dependencies = [ 169 | "libc", 170 | ] 171 | 172 | [[package]] 173 | name = "humantime" 174 | version = "2.1.0" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 177 | 178 | [[package]] 179 | name = "kernel32-sys" 180 | version = "0.2.2" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" 183 | dependencies = [ 184 | "winapi 0.2.8", 185 | "winapi-build", 186 | ] 187 | 188 | [[package]] 189 | name = "libc" 190 | version = "0.2.132" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" 193 | 194 | [[package]] 195 | name = "log" 196 | version = "0.4.17" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 199 | dependencies = [ 200 | "cfg-if", 201 | ] 202 | 203 | [[package]] 204 | name = "memchr" 205 | version = "2.5.0" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 208 | 209 | [[package]] 210 | name = "mio" 211 | version = "0.8.4" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" 214 | dependencies = [ 215 | "libc", 216 | "log", 217 | "wasi 0.11.0+wasi-snapshot-preview1", 218 | "windows-sys", 219 | ] 220 | 221 | [[package]] 222 | name = "num_cpus" 223 | version = "1.13.1" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" 226 | dependencies = [ 227 | "hermit-abi", 228 | "libc", 229 | ] 230 | 231 | [[package]] 232 | name = "once_cell" 233 | version = "1.14.0" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" 236 | 237 | [[package]] 238 | name = "parsed" 239 | version = "0.3.0" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "00b6c38c1dac6b10b3d2f5a5e8260487916444489439364802c8587dc2917301" 242 | 243 | [[package]] 244 | name = "ppv-lite86" 245 | version = "0.2.16" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" 248 | 249 | [[package]] 250 | name = "rand" 251 | version = "0.7.3" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" 254 | dependencies = [ 255 | "getrandom", 256 | "libc", 257 | "rand_chacha", 258 | "rand_core", 259 | "rand_hc", 260 | ] 261 | 262 | [[package]] 263 | name = "rand_chacha" 264 | version = "0.2.2" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" 267 | dependencies = [ 268 | "ppv-lite86", 269 | "rand_core", 270 | ] 271 | 272 | [[package]] 273 | name = "rand_core" 274 | version = "0.5.1" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" 277 | dependencies = [ 278 | "getrandom", 279 | ] 280 | 281 | [[package]] 282 | name = "rand_hc" 283 | version = "0.2.0" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" 286 | dependencies = [ 287 | "rand_core", 288 | ] 289 | 290 | [[package]] 291 | name = "regex" 292 | version = "1.6.0" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" 295 | dependencies = [ 296 | "aho-corasick", 297 | "memchr", 298 | "regex-syntax", 299 | ] 300 | 301 | [[package]] 302 | name = "regex-syntax" 303 | version = "0.6.27" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" 306 | 307 | [[package]] 308 | name = "strsim" 309 | version = "0.8.0" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 312 | 313 | [[package]] 314 | name = "termcolor" 315 | version = "1.1.3" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" 318 | dependencies = [ 319 | "winapi-util", 320 | ] 321 | 322 | [[package]] 323 | name = "textwrap" 324 | version = "0.11.0" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 327 | dependencies = [ 328 | "unicode-width", 329 | ] 330 | 331 | [[package]] 332 | name = "unicode-width" 333 | version = "0.1.10" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" 336 | 337 | [[package]] 338 | name = "uppercut" 339 | version = "0.4.0" 340 | dependencies = [ 341 | "bencher", 342 | "bytes", 343 | "clap", 344 | "core_affinity", 345 | "crossbeam-channel", 346 | "env_logger", 347 | "get_if_addrs", 348 | "log", 349 | "mio", 350 | "num_cpus", 351 | "parsed", 352 | "rand", 353 | ] 354 | 355 | [[package]] 356 | name = "vec_map" 357 | version = "0.8.2" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 360 | 361 | [[package]] 362 | name = "wasi" 363 | version = "0.9.0+wasi-snapshot-preview1" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 366 | 367 | [[package]] 368 | name = "wasi" 369 | version = "0.11.0+wasi-snapshot-preview1" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 372 | 373 | [[package]] 374 | name = "winapi" 375 | version = "0.2.8" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" 378 | 379 | [[package]] 380 | name = "winapi" 381 | version = "0.3.9" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 384 | dependencies = [ 385 | "winapi-i686-pc-windows-gnu", 386 | "winapi-x86_64-pc-windows-gnu", 387 | ] 388 | 389 | [[package]] 390 | name = "winapi-build" 391 | version = "0.1.1" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" 394 | 395 | [[package]] 396 | name = "winapi-i686-pc-windows-gnu" 397 | version = "0.4.0" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 400 | 401 | [[package]] 402 | name = "winapi-util" 403 | version = "0.1.5" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 406 | dependencies = [ 407 | "winapi 0.3.9", 408 | ] 409 | 410 | [[package]] 411 | name = "winapi-x86_64-pc-windows-gnu" 412 | version = "0.4.0" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 415 | 416 | [[package]] 417 | name = "windows-sys" 418 | version = "0.36.1" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" 421 | dependencies = [ 422 | "windows_aarch64_msvc", 423 | "windows_i686_gnu", 424 | "windows_i686_msvc", 425 | "windows_x86_64_gnu", 426 | "windows_x86_64_msvc", 427 | ] 428 | 429 | [[package]] 430 | name = "windows_aarch64_msvc" 431 | version = "0.36.1" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" 434 | 435 | [[package]] 436 | name = "windows_i686_gnu" 437 | version = "0.36.1" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" 440 | 441 | [[package]] 442 | name = "windows_i686_msvc" 443 | version = "0.36.1" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" 446 | 447 | [[package]] 448 | name = "windows_x86_64_gnu" 449 | version = "0.36.1" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" 452 | 453 | [[package]] 454 | name = "windows_x86_64_msvc" 455 | version = "0.36.1" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" 458 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "uppercut" 3 | version = "0.4.0" 4 | authors = ["sergey-melnychuk"] 5 | edition = "2018" 6 | description = "Small and simple actor model implementation" 7 | license = "MIT" 8 | documentation = "https://github.com/sergey-melnychuk/uppercut" 9 | homepage = "https://github.com/sergey-melnychuk/uppercut" 10 | repository = "https://github.com/sergey-melnychuk/uppercut" 11 | 12 | [profile.dev] 13 | panic = "unwind" 14 | 15 | [profile.release] 16 | panic = "unwind" 17 | 18 | [features] 19 | default = [] 20 | host = ["get_if_addrs"] 21 | 22 | [dependencies] 23 | mio = { version = "0.8.3", features = ["os-poll", "net"] } 24 | core_affinity = "0.5.10" 25 | crossbeam-channel = "0.5.4" 26 | parsed = "0.3.0" 27 | bytes = "1.1.0" 28 | get_if_addrs = { version = "0.5.3", optional = true } 29 | 30 | [dev-dependencies] 31 | bencher = "0.1.5" 32 | num_cpus = "1.13.0" 33 | clap = "2.33.1" 34 | rand = "0.7.3" 35 | log = "0.4" 36 | env_logger = "0.8.2" 37 | 38 | [[bench]] 39 | name = "workload" 40 | harness = false 41 | 42 | [[bench]] 43 | name = "mailbox" 44 | harness = false 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Sergey Melnychuk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://github.com/sergey-melnychuk/uppercut/workflows/Rust/badge.svg) 2 | 3 | ## Uppercut 4 | 5 | Simple and small actor model implementation. 6 | 7 | ### Install 8 | 9 | #### `Cargo.toml` 10 | 11 | ```toml 12 | [dependencies] 13 | uppercut = "0.4" 14 | ``` 15 | 16 | ### Example 17 | 18 | #### [`hello.rs`](/examples/hello.rs) 19 | 20 | ```shell 21 | $ cargo run --example hello 22 | [...] 23 | result: 42 24 | ``` 25 | 26 | #### [`pi.rs`](/examples/pi.rs) 27 | 28 | ```shell 29 | $ cargo run --release --example pi 30 | [...] 31 | Submitting 10000 workers making 100000 throws each. 32 | Pi estimate: 3.141561988 (in 5 seconds) 33 | ``` 34 | 35 | ### More examples 36 | 37 | - [remote](/examples/remote.rs) 38 | - [Gossip](/examples/gossip.rs) 39 | - [PAXOS](/examples/paxos.rs) 40 | -------------------------------------------------------------------------------- /benches/mailbox.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate bencher; 3 | use bencher::Bencher; 4 | use std::collections::{HashMap, VecDeque}; 5 | 6 | const ENTITIES: usize = 1024; 7 | const CAPACITY: usize = 128; 8 | const PREFIX: usize = 16; 9 | 10 | fn vec(b: &mut Bencher) { 11 | let mut map: HashMap> = HashMap::new(); 12 | for id in 0..ENTITIES { 13 | map.insert(id, Vec::with_capacity(CAPACITY)); 14 | } 15 | b.iter(|| { 16 | map.insert(0, Vec::with_capacity(CAPACITY)); 17 | let vec = map.get_mut(&0).unwrap(); 18 | for x in 0..CAPACITY { 19 | vec.push(x); 20 | } 21 | 22 | let mut queue = map.remove(&0).unwrap(); 23 | let remaining = queue.split_off(PREFIX); 24 | map.insert(0, remaining); 25 | 26 | assert_eq!( 27 | queue, 28 | vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] 29 | ); 30 | assert_eq!(map.remove(&0).unwrap().len(), CAPACITY - PREFIX); 31 | }); 32 | } 33 | 34 | fn deq(b: &mut Bencher) { 35 | let mut map: HashMap> = HashMap::new(); 36 | for id in 0..ENTITIES { 37 | map.insert(id, VecDeque::with_capacity(CAPACITY)); 38 | } 39 | b.iter(|| { 40 | map.insert(0, VecDeque::with_capacity(CAPACITY)); 41 | let deq = map.get_mut(&0).unwrap(); 42 | for x in 0..CAPACITY { 43 | deq.push_back(x); 44 | } 45 | 46 | let selected = deq.drain(..PREFIX).collect::>(); 47 | assert_eq!( 48 | selected, 49 | vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] 50 | ); 51 | assert_eq!(map.remove(&0).unwrap().len(), CAPACITY - PREFIX); 52 | }); 53 | } 54 | 55 | benchmark_group!(mailbox, vec, deq); 56 | benchmark_main!(mailbox); 57 | -------------------------------------------------------------------------------- /benches/workload.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate bencher; 3 | use bencher::Bencher; 4 | 5 | use std::sync::mpsc::{channel, Sender}; 6 | 7 | use uppercut::api::{AnyActor, AnySender, Envelope}; 8 | use uppercut::config::Config; 9 | use uppercut::core::System; 10 | use uppercut::pool::ThreadPool; 11 | 12 | fn counter(b: &mut Bencher) { 13 | #[derive(Default)] 14 | struct Test { 15 | count: usize, 16 | limit: usize, 17 | tx: Option>, 18 | } 19 | 20 | #[derive(Debug)] 21 | enum Protocol { 22 | Init(usize, Sender), 23 | Hit, 24 | } 25 | 26 | impl AnyActor for Test { 27 | fn receive(&mut self, envelope: Envelope, sender: &mut dyn AnySender) { 28 | if let Some(p) = envelope.message.downcast_ref::() { 29 | match p { 30 | Protocol::Init(limit, tx) => { 31 | self.limit = *limit; 32 | self.tx = Some(tx.to_owned()); 33 | sender.send(sender.me(), Envelope::of(Protocol::Hit).from(sender.me())); 34 | } 35 | Protocol::Hit if self.count < self.limit => { 36 | self.count += 1; 37 | sender.send(sender.me(), Envelope::of(Protocol::Hit).from(sender.me())); 38 | } 39 | Protocol::Hit => { 40 | self.tx.take().unwrap().send(self.count).unwrap(); 41 | } 42 | } 43 | } 44 | } 45 | } 46 | 47 | let cfg = Config::default(); 48 | let pool: ThreadPool = ThreadPool::for_config(&cfg); 49 | b.iter(|| { 50 | let sys = System::new("bench", "localhost", &cfg); 51 | let run = sys.run(&pool).unwrap(); 52 | 53 | let (tx, rx) = channel(); 54 | run.spawn_default::("test"); 55 | run.send("test", Envelope::of(Protocol::Init(1000, tx))); 56 | 57 | rx.recv().unwrap(); 58 | 59 | run.shutdown(); 60 | }); 61 | } 62 | 63 | fn chain(b: &mut Bencher) { 64 | const LENGTH: usize = 1000; 65 | 66 | #[derive(Debug)] 67 | struct Hit(Sender, usize); 68 | 69 | #[derive(Default)] 70 | struct Chain; 71 | 72 | impl AnyActor for Chain { 73 | fn receive(&mut self, envelope: Envelope, sender: &mut dyn AnySender) { 74 | if let Some(Hit(tx, hits)) = envelope.message.downcast_ref::() { 75 | if *hits < LENGTH { 76 | let tag = format!("{}", hits + 1); 77 | sender.spawn(&tag, Box::new(|| Box::new(Chain))); 78 | let env = Envelope::of(Hit(tx.to_owned(), hits + 1)); 79 | sender.send(&tag, env); 80 | } else { 81 | tx.send(*hits).unwrap(); 82 | } 83 | } 84 | } 85 | } 86 | 87 | let cfg = Config::default(); 88 | let pool: ThreadPool = ThreadPool::for_config(&cfg); 89 | b.iter(|| { 90 | let sys = System::new("bench", "localhost", &cfg); 91 | let run = sys.run(&pool).unwrap(); 92 | 93 | let (tx, rx) = channel(); 94 | run.spawn_default::("0"); 95 | run.send("0", Envelope::of(Hit(tx, 0))); 96 | 97 | rx.recv().unwrap(); 98 | 99 | run.shutdown(); 100 | }); 101 | } 102 | 103 | benchmark_group!(workload, counter, chain); 104 | benchmark_main!(workload); 105 | -------------------------------------------------------------------------------- /doc/design-guide.md: -------------------------------------------------------------------------------- 1 | ### Design 2 | 3 | #### Introduction 4 | 5 | This document introduces basic concepts and design decisions behind standalone 6 | actor model implementation in Rust. 7 | 8 | #### Core concepts 9 | 10 | Message passing is the way we humans communicate everywhere, and it is pretty much 11 | the only possible way of communication that scales from individuals to nations or 12 | even whole World. 13 | 14 | One of the institutions that has message passing as its core is a Post Office. 15 | Model and abstractions used in this document are very similar, like messages 16 | and envelopes. 17 | 18 | Fundamental entities are: 19 | - Address - logical point where Envelope can be delivered 20 | - Envelope - a container to hold: 21 | - Message content 22 | - no restrictions apply on message content 23 | - Address of the sender of the Message 24 | - Address of the receiver of the Message 25 | - Actor - a reactive (passive) entity that: 26 | - has a known Address 27 | - when receive an Envelope can: 28 | - access (change) own internal state 29 | - create (spawn) new Actors 30 | - send Envelope to a known Address 31 | - Environment - a machinery that delivers Envelopes to Actors 32 | - collects (hopefully) meaningful metrics along the way 33 | 34 | Important comments: 35 | - Envelope makes no restrictions on Message content 36 | - Post Office: anyone can send anything to a known Address 37 | - This means that generic Actor can receive anything 38 | - Which in turn means that such Actor cannot be type-safe! 39 | - So type assertions on received Message is part of Actor's internal behavior 40 | 41 | Each Actor holds its state internally, thus there is no direct way to access 42 | other's Actor internal state. This leaves message passing as the only way for 43 | Actors to interact with each other and with the Environment. 44 | 45 | Actor is reactive (passive) by nature, it can only "act" when asked by the 46 | Environment to process the Envelope. 47 | 48 | Actor and Address are absolutely independent and do not rely on each other. 49 | The Environment is responsible for binding an Actor under specific Address, 50 | and thus enforcing all Envelopes sent to a given Address to be received by 51 | specific Actor, bound to that specific Address. The Environment is also 52 | responsible for letting the Actor know under which address it was bound. 53 | 54 | Until an Actor is bound under specific Address, it cannot receive any messages - 55 | thus cannot perform any action, so it is not distinguishable if such Actor even 56 | exists, as the only way to check it would be to send an Envelope to an Address. 57 | As long as the Environment is responsible for such binding, Actor can only be 58 | created inside the Environment, which makes the Environment owner of all Actors! 59 | 60 | In order to perform any task, the Environment must provide a way to send an 61 | Envelope to an Address from the outside - otherwise no single Actor can ever act, 62 | as in order to act, an Envelope must be received first. In order to be received, 63 | an Envelope then must be sent first! 64 | 65 | With provided ways of (1) spawning an actor in declarative manner (as actors can 66 | be created only inside the Environment, not outside) under given Address and (2) 67 | sending an Envelope to a given Address, it must be possible to define initial 68 | configuration of the Environment: set of Actor blueprints and initial set of 69 | Envelopes to be sent to respective Addresses. 70 | 71 | After that, what's left is to somehow start the Environment (actor runtime), and 72 | allow it to have access to required resources. If failed to start, the initial 73 | configuration remains, thus can be restarted any required number of attempts. 74 | 75 | #### DONE 76 | 77 | 1. Remote Actors (based on IO-bridge) 78 | - Address is still regular String: local-actor-address@host:port 79 | - Example: connection-0123@192.168.1.2:9000 80 | - Message must be (de)serializable to (from) bytes (serde/bincode/...) 81 | - Sending: send Envelope with `Vec` to Remote Actor 82 | - Environment detects that such an Envelope must go through IO-bridge 83 | - Receiving: IO-bridge reads from TCP connection 84 | - Environment forwards received buffer to local Actor address 85 | - [remote example](https://github.com/sergey-melnychuk/uppercut/blob/master/examples/remote.rs) 86 | 1. IO-bridge abstraction (based on TCP server impl) 87 | - TCP server listens for incoming messages from remote actors 88 | - TCP clients send messages to remote actors 89 | 1. Basic implementation of TCP server on top of Actors 90 | - TCP listener is an Actor 91 | - connection listeners are Actors 92 | - parsing bytes to HTTP/WebSocket frames 93 | - serializing WebSocket frames to bytes 94 | - [uppercut-mio-server](https://github.com/sergey-melnychuk/uppercut-lab/tree/master/uppercut-mio-server) 95 | 1. Shutdown the Environment 96 | - immediate: 97 | - stop all actors 98 | - drop all undelivered messages 99 | - graceful (wait for all tasks to complete) 100 | - impossible in general case, Actor Model is non-deterministic 101 | - **a graceful shutdown is application-specific** 102 | 1. Stop an Actor under specific Address 103 | - `sender.stop("addr")` 104 | - only local (not remote) actors can be stopped like this (as for now) 105 | - actor can request to stop itself using the same approach 106 | 1. Basic metrics reporting: 107 | - Default behaviour: dump metrics to stdout with configurable interval 108 | 1. Actor failure handling: 109 | - `std::panic::catch_unwind` ([doc](https://doc.rust-lang.org/std/panic/fn.catch_unwind.html)) 110 | - `AssertUnwindSafe` ([doc](https://doc.rust-lang.org/std/panic/struct.AssertUnwindSafe.html)) 111 | - The `on_fail` method of `AnyActor` is fired when actor failure is detected 112 | - Providing custom impl of `on_fail` allows signaling/propagation of the error (if needed) 113 | - Generally it is hard to distinguish recoverable and non-recoverable failures 114 | - What is not part of a domain seems like non-recoverable failure 115 | - Supervision concept thus moves to application domain and client code is responsible for implementing it 116 | - see [fail](/examples/fail.rs) example for more details 117 | 1. Actor stop watch: 118 | - The `on_stop` method of `AnyActor` is called right before an actor is permanently stopped 119 | - This is the place where to clean up any existing resources and signal stopped processing 120 | - see [stop](/examples/stop.rs) example for more details 121 | 1. Raw Actors: 122 | - Message is just a byte buffer 123 | - serialize and send byte buffer 124 | - receive byte buffer and try deserialize 125 | - de-/serialization 126 | - no restrictions on specific impl 127 | - [bincode](https://github.com/servo/bincode) 128 | 1. Centralized unified Logging & Metrics: 129 | - Simple structured Metrics and Logging model provided 130 | - Logs from actors are collected and dumped to stdout (if enabled) in the event loop 131 | - Scheduler metrics are collected and dumped to stdout (if enabled) in the event loop 132 | - Custom metrics: same as Logging 133 | - TODO: Expose logging/metrics DTOs with JSON support 134 | - Logs/metrics can be sent as JSON over UDP for aggregation 135 | - To keep in mind: Tracing support? 136 | 1. Example implementations: 137 | - [PAXOS](/examples/paxos.rs) (distributed consensus - simple log replication) 138 | - [Gossip](/examples/gossip.rs) (heartbeat gossip distributed membership and failure detection) 139 | - TODO Distributed Lock 140 | - TODO Raft (leader election) 141 | - TODO Distributed Hash-Table/KV-store (consistent-hashing/chord, replication/persistence) 142 | - Bucket = Actor, Request = Actor 143 | - Transactions (multi-key updates)? 144 | - TODO Distributed Message Queue (routing, replication, persistence) 145 | 146 | #### TODO 147 | 148 | 1. Add API for generic TCP-server: 149 | - Does not have to be included in the uppercut: 150 | - [example](https://github.com/sergey-melnychuk/uppercut-lab/blob/master/uppercut-mio-server/src/server.rs) 151 | - Break down into `Connection` and `Handler`: 152 | - uppercut-provided `Connection` actor-per-connection (receives `Connect`/`Recv`/`Send`/stop) 153 | - client-provided `Handler` (spawned by `Connection`, aware of the parent `Connection`) 154 | - `Handler` is responsible for buffering/matching/processing/spawning-worker-actors/etc 155 | 1. Test-kit: 156 | - Allow probing for specific messages at specific Address 157 | - Allow accessing Actor's internal state (for tests only) 158 | 1. Persistence: 159 | - Message queue that can be persisted to disk 160 | - Allows introduction of reliability guarantees 161 | - Configurable backend? (leveldb/rocksdb/etc) 162 | - Custom lightweight append-log storage engine? 163 | - KV mode: key-value based? 164 | - MQ mode: append-log based? 165 | 1. Insights into Environment internals (Admin Panel) 166 | - Extend existing metric collection approach 167 | - Allow narrowing down tracing to specific Address 168 | - get sent/received/processed rate 169 | - get dump of specific messages processed by the Actor 170 | - get dump of Actor's current internal state 171 | - Consider running such actors on standalone thread-pools 172 | - minimize tracing overhead for the whole system 173 | - Actor can be easily migrated between thread-pools 174 | - Dashboard representing current state of the Environment? 175 | - standalone web-page? 176 | - Grafana dashboard? 177 | - [questdb](https://questdb.io/) 178 | 1. Address 100% usage of all provided CPU cores. 179 | - Burning down the CPU - but for performance! 180 | - Acceptable in virtualized environments or containers? 181 | 182 | #### Experiments 183 | 184 | 1. Queue-per-worker-thread scheduler (instead of a work queue shared between worker threads) 185 | - The main thread keeps a separate work queue for each worker thread 186 | - Must allow better scaling to higher number of threads (current 'magic number' is 4) 187 | - No contention between workers for a shared work queue 188 | - More flexibility in routing between workers (round-robin, fastest-turnaround) 189 | - Collecting some metrics from worker threads might make sense for routing heuristics 190 | - Overhead from introducing N bounded channels seems to be negligible 191 | - Benchmark: uppercut-mio-server VS. hello-world in actix-web 192 | 1. Binding Actor to specific worker thread 193 | - Messages between 'local' actors (bound to the same thread) do not leave the thread 194 | - Main thread only binds actors and routes 'overflown' (not local) messages 195 | - Allows introduction of routing/binding strategies: 196 | - Thread groups dedicated to: actors/logs/metrics/io/db/cpu/custom 197 | - Work distribution strategies to try: round-robin, smallest-counter, target-tag etc. 198 | - Migration of Actors between Workers (re-balancing the Actor System) 199 | - Unlucky binding might not benefit from same-thread-actor locality 200 | - Navigate frequent pairs/sequences of messages and bind them to the same thread 201 | 1. Run prod-like workload once KV/MQ impl is ready 202 | 203 | #### Results 204 | 205 | 1. Batch-processing of Actions on the main loop 206 | - For the reference it is kept in `feature/batch-actions` 207 | - Got ~20% lower throughput according to 'full' example 208 | 209 | #### References 210 | 211 | 1. Hewitt, Carl; Bishop, Peter; Steiger, Richard (1973). ["A Universal Modular Actor Formalism for Artificial Intelligence". IJCAI.]( 212 | https://www.ijcai.org/Proceedings/73/Papers/027B.pdf) 213 | 214 | 1. Hewitt, Meijer and Szyperski: The Actor Model (everything you wanted to know, but were afraid to ask) [video](https://www.youtube.com/watch?v=7erJ1DV_Tlo) 215 | 216 | 1. Wikipedia: [Actor Model](https://en.wikipedia.org/wiki/Actor_model) 217 | -------------------------------------------------------------------------------- /doc/log-and-mon.md: -------------------------------------------------------------------------------- 1 | #### Logging and Monitoring 2 | 3 | ##### Idea 4 | 5 | Built-in log aggregation and metrics reporting - possibly overridden by exporters to other services (e.g. logstash, prometheus etc). 6 | 7 | Simply reading stream of lines (separated by `\n`) from TCP and storing them to time series DB (in memory even) should be enough. 8 | 9 | Persistence options: QuestDB (metrics), Cassandra (logging), flat files (logging). 10 | 11 | Flat files might be super easy to start with for both metrics and logging. 12 | Logging as plain text line by line to `/path/to/logs/host=192.168.42.42/app=server/topic=connections/tag=connection-00001/1599041920.log`. 13 | Metrics flushed to binary file (fixed-length value: 8 bytes (double)): `/path/to/metrics/host=192.168.42.42/app=server/topic=connections/tag=connection-00001/1599041920.bin`. 14 | 15 | Indexing/querying: ??? 16 | 17 | Admin API: increase logging level, enable/disable logging all messages (e.g. for specific Actor). 18 | 19 | ##### Misc 20 | 21 | ```rust 22 | trait Logger { 23 | fn log<'a>(&mut self, tag: &str, message: &dyn Fn() -> String); 24 | } 25 | 26 | #[derive(Default)] 27 | struct LocalLogger; 28 | 29 | impl Logger for LocalLogger { 30 | fn log<'a>(&mut self, tag: &str, msg: &dyn Fn() -> String) { 31 | println!("2020-09-02 12:34:56.789 [host=127.0.0.1 app=server topic=connection tag={}] {}", tag, msg()); 32 | } 33 | } 34 | 35 | fn main() { 36 | let mut logger = LocalLogger::default(); 37 | let x = 42; 38 | logger.log("connection-00001", &|| format!("Client connection received from 192.168.1.123:56123, x={:?}", x)); 39 | } 40 | ``` 41 | 42 | A unified format for logs and metrics, prometheus-friendly: 43 | 44 | ```json 45 | {"at":1599041920,"meta":{"host":"","app":"server","topic":"events","tag":"connection-00001"}, 46 | "log":"Client connection received from 192.168.1.123:56123, x=42"} 47 | ``` 48 | 49 | ```json 50 | {"at":1599041920,"meta":{"host":"","app":"server","topic":"tick"},"val":669207.0} 51 | {"at":1599041920,"meta":{"host":"","app":"server","topic":"miss"},"val":0.0} 52 | {"at":1599041920,"meta":{"host":"","app":"server","topic":"hit"},"val":669207.0} 53 | {"at":1599041920,"meta":{"host":"","app":"server","topic":"messages"},"val":334433.0} 54 | {"at":1599041920,"meta":{"host":"","app":"server","topic":"queues"},"val":334433.0} 55 | {"at":1599041920,"meta":{"host":"","app":"server","topic":"returns"},"val":334691.0} 56 | {"at":1599041920,"meta":{"host":"","app":"server","topic":"spawns"},"val":0.0} 57 | {"at":1599041920,"meta":{"host":"","app":"server","topic":"delays"},"val":83.0} 58 | {"at":1599041920,"meta":{"host":"","app":"server","topic":"stops"},"val":0.0} 59 | {"at":1599041920,"meta":{"host":"","app":"server","topic":"actors"},"val":100004.0} 60 | ``` 61 | 62 | Lightweight approach to define local machines IP (if any): 63 | 64 | ```rust 65 | // get_if_addrs = "0.5" 66 | 67 | extern crate get_if_addrs; 68 | 69 | use std::net::IpAddr; 70 | 71 | fn main() { 72 | let ip = get_if_addrs::get_if_addrs() 73 | .unwrap_or_default() 74 | .into_iter() 75 | .find(|i| !i.is_loopback()) 76 | .map(|i| i.addr.ip()) 77 | .unwrap_or(IpAddr::from([127, 0, 0, 1])); 78 | 79 | println!("IP: {}", ip.to_string()); 80 | } 81 | ``` 82 | -------------------------------------------------------------------------------- /examples/fail.rs: -------------------------------------------------------------------------------- 1 | use std::sync::mpsc::{channel, Sender}; 2 | 3 | extern crate uppercut; 4 | use std::any::Any; 5 | use std::thread::sleep; 6 | use std::time::Duration; 7 | use uppercut::api::{AnyActor, AnySender, Envelope}; 8 | use uppercut::config::{Config, SchedulerConfig}; 9 | use uppercut::core::System; 10 | use uppercut::pool::ThreadPool; 11 | 12 | #[derive(Debug)] 13 | struct Message(Option, Sender); 14 | 15 | #[derive(Default)] 16 | struct State; 17 | 18 | impl AnyActor for State { 19 | fn receive(&mut self, envelope: Envelope, sender: &mut dyn AnySender) { 20 | if let Some(msg) = envelope.message.downcast_ref::() { 21 | sender.log(&format!("received: {:?}", msg)); 22 | let x = msg.0.unwrap(); 23 | msg.1.send(x).unwrap(); 24 | } 25 | } 26 | 27 | fn on_fail(&self, _error: Box, sender: &mut dyn AnySender) { 28 | sender.log("failure detected!"); 29 | } 30 | 31 | fn on_stop(&self, sender: &mut dyn AnySender) { 32 | sender.log("shutting down"); 33 | } 34 | } 35 | 36 | fn main() { 37 | let cfg = Config::new( 38 | SchedulerConfig { 39 | eager_shutdown_enabled: false, 40 | ..Default::default() 41 | }, 42 | Default::default(), 43 | ); 44 | let sys = System::new("fail-example", "localhost", &cfg); 45 | let pool = ThreadPool::new(6); 46 | let run = sys.run(&pool).unwrap(); 47 | run.spawn_default::("x"); 48 | 49 | let (tx, rx) = channel(); 50 | run.send("x", Envelope::of(Message(Some(42), tx.clone()))); 51 | run.send("x", Envelope::of(Message(None, tx.clone()))); 52 | run.send("x", Envelope::of(Message(Some(100500), tx))); 53 | 54 | println!("recv: {}", rx.recv().unwrap()); 55 | sleep(Duration::from_secs(3)); 56 | run.shutdown(); 57 | } 58 | -------------------------------------------------------------------------------- /examples/full.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | use std::time::{Duration, Instant}; 3 | 4 | extern crate uppercut; 5 | use uppercut::api::{AnyActor, AnySender, Envelope}; 6 | use uppercut::config::{Config, RemoteConfig, SchedulerConfig}; 7 | use uppercut::core::System; 8 | use uppercut::pool::ThreadPool; 9 | 10 | extern crate num_cpus; 11 | 12 | struct Round { 13 | size: usize, 14 | } 15 | 16 | impl Round { 17 | fn new(size: usize) -> Round { 18 | Round { size } 19 | } 20 | } 21 | 22 | #[derive(Debug)] 23 | struct Hit(usize); 24 | 25 | #[derive(Clone, Debug)] 26 | struct Acc { 27 | name: String, 28 | zero: usize, 29 | hits: usize, 30 | } 31 | 32 | #[derive(Debug)] 33 | enum Fan { 34 | Trigger { size: usize }, 35 | Out { id: usize }, 36 | In { id: usize }, 37 | } 38 | 39 | #[derive(Default)] 40 | struct Root { 41 | size: usize, 42 | count: usize, 43 | epoch: usize, 44 | seen: HashSet, 45 | } 46 | 47 | impl AnyActor for Root { 48 | fn receive(&mut self, envelope: Envelope, sender: &mut dyn AnySender) { 49 | if let Some(fan) = envelope.message.downcast_ref::() { 50 | match fan { 51 | Fan::In { id } => { 52 | self.seen.insert(*id); 53 | self.count += 1; 54 | if self.count == self.size { 55 | self.seen.clear(); 56 | self.count = 0; 57 | sender.log(&format!( 58 | "root completed the fanout of size: {} (epoch: {})", 59 | self.size, self.epoch 60 | )); 61 | let trigger = Fan::Trigger { size: self.size }; 62 | let env = Envelope::of(trigger).from(sender.me()); 63 | sender.send(sender.me(), env); 64 | self.epoch += 1; 65 | } 66 | } 67 | Fan::Trigger { size } => { 68 | self.size = *size; 69 | for id in 0..self.size { 70 | let tag = format!("{}", id); 71 | let env = Envelope::of(Fan::Out { id }).from(sender.me()); 72 | sender.send(&tag, env) 73 | } 74 | } 75 | _ => (), 76 | } 77 | } 78 | } 79 | } 80 | 81 | impl AnyActor for Round { 82 | fn receive(&mut self, envelope: Envelope, sender: &mut dyn AnySender) { 83 | if let Some(hit) = envelope.message.downcast_ref::() { 84 | let next = (hit.0 + 1) % self.size; 85 | let tag = format!("{}", next); 86 | let env = Envelope::of(Hit(hit.0 + 1)).from(sender.me()); 87 | sender.send(&tag, env); 88 | } else if let Some(acc) = envelope.message.downcast_ref::() { 89 | let next = (acc.zero + acc.hits + 1) % self.size; 90 | let tag = format!("{}", next); 91 | let msg = Acc { 92 | name: acc.name.clone(), 93 | zero: acc.zero, 94 | hits: acc.hits + 1, 95 | }; 96 | let env = Envelope::of(msg).from(sender.me()); 97 | sender.send(&tag, env); 98 | } else if let Some(Fan::Out { id }) = envelope.message.downcast_ref::() { 99 | let env = Envelope::of(Fan::In { id: *id }).from(sender.me()); 100 | sender.send(&envelope.from, env); 101 | } else { 102 | sender.log(&format!( 103 | "unexpected message: {:?}", 104 | envelope.message.type_id() 105 | )); 106 | } 107 | } 108 | } 109 | 110 | struct Periodic { 111 | at: Instant, 112 | timings: HashMap, 113 | counter: usize, 114 | } 115 | 116 | impl Periodic { 117 | fn report(&self) -> (usize, usize, usize, usize) { 118 | let mut ds = self.timings.keys().collect::>(); 119 | ds.sort(); 120 | let min = *ds[0]; 121 | let max = *ds[ds.len() - 1]; 122 | let p50 = *ds[(ds.len() - 1) / 2]; 123 | let p99 = *ds[(ds.len() - 1) * 99 / 100]; 124 | (min, max, p50, p99) 125 | } 126 | } 127 | 128 | impl Default for Periodic { 129 | fn default() -> Self { 130 | Periodic { 131 | at: Instant::now(), 132 | timings: HashMap::new(), 133 | counter: 0, 134 | } 135 | } 136 | } 137 | 138 | #[derive(Debug)] 139 | struct Tick { 140 | at: Instant, 141 | } 142 | 143 | impl AnyActor for Periodic { 144 | fn receive(&mut self, envelope: Envelope, sender: &mut dyn AnySender) { 145 | if let Some(Tick { at }) = envelope.message.downcast_ref::() { 146 | self.at = Instant::now(); 147 | let d = self.at.duration_since(*at).as_millis() as usize; 148 | if let Some(n) = self.timings.get_mut(&d) { 149 | *n += 1; 150 | } else { 151 | self.timings.insert(d, 1); 152 | } 153 | self.counter += 1; 154 | if self.counter % 1000 == 0 { 155 | let (min, max, p50, p99) = self.report(); 156 | sender.log(&format!("min={} p50={} p99={} max={}", min, p50, p99, max)); 157 | self.timings.clear(); 158 | } 159 | let env = Envelope::of(Tick { at: Instant::now() }).from(sender.me()); 160 | let delay = Duration::from_millis(10); 161 | sender.delay(sender.me(), env, delay); 162 | } 163 | } 164 | } 165 | 166 | #[derive(Default)] 167 | struct PingPong { 168 | count: usize, 169 | } 170 | 171 | impl AnyActor for PingPong { 172 | fn receive(&mut self, envelope: Envelope, sender: &mut dyn AnySender) { 173 | if let Some(s) = envelope.message.downcast_ref::() { 174 | if self.count % 1000 == 0 { 175 | sender.log(&format!( 176 | "Actor '{}' (count={}) received message '{}'", 177 | sender.me(), 178 | self.count, 179 | s 180 | )); 181 | } 182 | self.count += 1; 183 | if s == "ping" { 184 | let r = Envelope::of("pong".to_string()).from(sender.me()); 185 | sender.send(&envelope.from, r); 186 | } else if s == "pong" { 187 | let r = Envelope::of("ping".to_string()).from(sender.me()); 188 | sender.send(&envelope.from, r); 189 | } 190 | } 191 | } 192 | } 193 | 194 | fn main() { 195 | // Max throughput seems to be achieved with 4 worker threads on 8+ cores machine. 196 | let cores = std::cmp::min(4, num_cpus::get()); 197 | let pool = ThreadPool::new(cores + 2); // +1 event loop, +1 offload thread 198 | 199 | let mut scheduler_config = SchedulerConfig::with_total_threads(cores); 200 | scheduler_config.metric_reporting_enabled = true; 201 | let cfg = Config::new(scheduler_config, RemoteConfig::default()); 202 | let sys = System::new("full", "localhost", &cfg); 203 | let run = sys.run(&pool).unwrap(); 204 | 205 | const SIZE: usize = 100_000; 206 | for id in 0..SIZE { 207 | let tag = format!("{}", id); 208 | run.spawn(&tag, || Box::new(Round::new(SIZE))); 209 | } 210 | 211 | run.send("0", Envelope::of(Hit(0))); 212 | 213 | for id in 0..1000 { 214 | let tag = format!("{}", id); 215 | let acc = Acc { 216 | name: tag.clone(), 217 | zero: id, 218 | hits: 0, 219 | }; 220 | let env = Envelope::of(acc).from(&tag); 221 | run.send(&tag, env); 222 | } 223 | 224 | run.spawn_default::("root"); 225 | let env = Envelope::of(Fan::Trigger { size: SIZE }).from("root"); 226 | run.send("root", env); 227 | 228 | run.spawn_default::("timer"); 229 | let tick = Envelope::of(Tick { at: Instant::now() }).from("timer"); 230 | run.delay("timer", tick, Duration::from_secs(10)); 231 | 232 | run.spawn_default::("ping"); 233 | run.spawn_default::("pong"); 234 | 235 | let ping = Envelope::of("ping".to_string()).from("pong"); 236 | run.send("ping", ping); 237 | 238 | std::thread::park(); // block current thread (https://doc.rust-lang.org/std/thread/fn.park.html) 239 | } 240 | -------------------------------------------------------------------------------- /examples/gossip.rs: -------------------------------------------------------------------------------- 1 | extern crate log; 2 | use env_logger::fmt::TimestampPrecision; 3 | use log::{debug, info, warn}; 4 | use std::collections::HashSet; 5 | 6 | extern crate bytes; 7 | use bytes::{Buf, BufMut, Bytes, BytesMut}; 8 | 9 | use crossbeam_channel::{bounded, Sender}; 10 | 11 | extern crate uppercut; 12 | use std::time::{Duration, Instant}; 13 | use uppercut::api::{AnyActor, AnySender, Envelope}; 14 | use uppercut::config::Config; 15 | use uppercut::core::{Run, System}; 16 | use uppercut::pool::ThreadPool; 17 | 18 | #[derive(Debug, Default, Clone)] 19 | struct Peer { 20 | tag: String, 21 | beat: u64, 22 | seen: u64, 23 | } 24 | 25 | #[derive(Debug)] 26 | struct Agent { 27 | tag: String, 28 | beat: u64, 29 | peers: Vec, 30 | down: HashSet, 31 | horizon: u64, 32 | period: u64, 33 | tick: u64, 34 | zero: Instant, 35 | countdown: String, 36 | } 37 | 38 | impl Default for Agent { 39 | fn default() -> Self { 40 | Self { 41 | tag: Default::default(), 42 | beat: Default::default(), 43 | peers: Default::default(), 44 | down: Default::default(), 45 | horizon: Default::default(), 46 | period: Default::default(), 47 | tick: Default::default(), 48 | zero: Instant::now(), 49 | countdown: Default::default(), 50 | } 51 | } 52 | } 53 | 54 | #[derive(Debug)] 55 | enum Event { 56 | New(String), 57 | Out(String), 58 | } 59 | 60 | impl Agent { 61 | fn gossip(&self, time: u64) -> Vec<(String, u64)> { 62 | let peers: Vec<(String, u64)> = self 63 | .peers 64 | .iter() 65 | .filter(|peer| peer.seen + self.horizon >= time) 66 | .map(|peer| (peer.tag.clone(), peer.beat)) 67 | .collect(); 68 | if peers.len() < 2 { 69 | vec![(self.tag.clone(), self.beat)] 70 | } else { 71 | vec![ 72 | peers[self.beat as usize % peers.len()].clone(), 73 | (self.tag.clone(), self.beat), 74 | ] 75 | } 76 | } 77 | 78 | fn detect(&mut self, time: u64) -> Vec { 79 | let mut events = Vec::new(); 80 | for peer in self.peers.iter() { 81 | if peer.seen + self.horizon < time && !self.down.contains(&peer.tag) { 82 | // If a peer stays down, do not report it every detection. 83 | // TODO Handle case when a peer gets back up again. 84 | self.down.insert(peer.tag.clone()); 85 | events.push(Event::Out(peer.tag.clone())); 86 | } 87 | } 88 | events 89 | } 90 | 91 | fn accept(&mut self, time: u64, gossip: Vec<(String, u64)>) -> Vec { 92 | let mut events = Vec::new(); 93 | for (tag, beat) in gossip { 94 | if tag == self.tag { 95 | continue; 96 | } 97 | let known = self.peers.iter_mut().find(|p| p.tag == tag); 98 | if let Some(peer) = known { 99 | if beat > peer.beat { 100 | peer.beat = beat; 101 | peer.seen = time; 102 | } 103 | } else { 104 | // new (previously unseen) peer is introduced through the gossip 105 | events.push(Event::New(tag.clone())); 106 | self.peers.push(Peer { 107 | tag, 108 | beat, 109 | seen: time, 110 | }); 111 | } 112 | } 113 | 114 | events.append(&mut self.detect(time)); 115 | events 116 | } 117 | } 118 | 119 | #[derive(Debug, Clone)] 120 | enum Message { 121 | Ping(String, u64), 122 | Pong(String, u64), 123 | Gossip(Vec<(String, u64)>), 124 | Stop, 125 | 126 | Tick, 127 | Init(u64, u64, u64, Vec, String), 128 | } 129 | 130 | impl From for Vec { 131 | fn from(message: Message) -> Self { 132 | let mut buf = BytesMut::new(); 133 | match message { 134 | Message::Ping(target, beat) => { 135 | buf.put_u8(1); 136 | buf.put_u8(target.len() as u8); 137 | buf.put_slice(target.as_bytes()); 138 | buf.put_u64(beat); 139 | } 140 | Message::Pong(target, beat) => { 141 | buf.put_u8(2); 142 | buf.put_u8(target.len() as u8); 143 | buf.put_slice(target.as_bytes()); 144 | buf.put_u64(beat); 145 | } 146 | Message::Gossip(pairs) => { 147 | buf.put_u8(3); 148 | buf.put_u8(pairs.len() as u8); 149 | pairs.into_iter().for_each(|(tag, beat)| { 150 | buf.put_u8(tag.len() as u8); 151 | buf.put_slice(tag.as_bytes()); 152 | buf.put_u64(beat); 153 | }); 154 | } 155 | Message::Stop => { 156 | buf.put_u8(4); 157 | } 158 | _ => { 159 | buf.put_u8(0); 160 | } 161 | } 162 | buf.split().to_vec() 163 | } 164 | } 165 | 166 | impl From> for Message { 167 | fn from(vec: Vec) -> Self { 168 | let mut buf = Bytes::from(vec); 169 | let op = buf.get_u8(); 170 | match op { 171 | 1 => { 172 | let n = buf.get_u8(); 173 | let v = buf.copy_to_bytes(n as usize).to_vec(); 174 | let b = buf.get_u64(); 175 | Message::Ping(String::from_utf8(v).unwrap(), b) 176 | } 177 | 2 => { 178 | let n = buf.get_u8(); 179 | let v = buf.copy_to_bytes(n as usize).to_vec(); 180 | let b = buf.get_u64(); 181 | Message::Pong(String::from_utf8(v).unwrap(), b) 182 | } 183 | 3 => { 184 | let k = buf.get_u8(); 185 | let peers: Vec<(String, u64)> = (0..k) 186 | .into_iter() 187 | .map(|_| { 188 | let n = buf.get_u8(); 189 | let v = buf.copy_to_bytes(n as usize).to_vec(); 190 | let b = buf.get_u64(); 191 | (String::from_utf8(v).unwrap(), b) 192 | }) 193 | .collect(); 194 | Message::Gossip(peers) 195 | } 196 | 4 => Message::Stop, 197 | _ => Message::Tick, 198 | } 199 | } 200 | } 201 | 202 | impl AnyActor for Agent { 203 | fn receive(&mut self, envelope: Envelope, sender: &mut dyn AnySender) { 204 | let time = self.zero.elapsed().as_millis() as u64; 205 | if let Some(buf) = envelope.message.downcast_ref::>() { 206 | let message: Message = buf.clone().into(); 207 | info!( 208 | "tag={} time={} msg={:?} from={}\n\tpeers={:?}", 209 | sender.me(), 210 | time, 211 | message, 212 | envelope.from, 213 | self.peers 214 | ); 215 | match message { 216 | Message::Gossip(peers) => { 217 | let events = self.accept(time, peers); 218 | for e in events { 219 | warn!("\ttag={} event={:?}", sender.me(), e); 220 | } 221 | } 222 | Message::Ping(me, beat) => { 223 | self.tag = me; 224 | let is_known = self.peers.iter().any(|p| p.tag == sender.me()); 225 | let is_self = sender.me() == envelope.from; 226 | if !is_self && !is_known { 227 | let pong: Vec = Message::Pong(envelope.from.clone(), self.beat).into(); 228 | sender.send(&envelope.from, Envelope::of(pong).from(sender.me())); 229 | let events = self.accept(time, vec![(envelope.from, beat)]); 230 | for e in events { 231 | warn!("\ttag={} event={:?}", sender.me(), e); 232 | } 233 | } 234 | } 235 | Message::Pong(me, beat) => { 236 | self.tag = me; 237 | let is_known = self.peers.iter().any(|p| p.tag == sender.me()); 238 | let is_self = sender.me() == envelope.from; 239 | if !is_self && !is_known { 240 | let events = self.accept(time, vec![(envelope.from, beat)]); 241 | for e in events { 242 | warn!("\ttag={} event={:?}", sender.me(), e); 243 | } 244 | } 245 | } 246 | Message::Stop => { 247 | warn!("tag={} event=stop", sender.me()); 248 | sender.stop(sender.me()); 249 | let down: Vec = Down(sender.me().to_string()).into(); 250 | sender.send(&self.countdown, Envelope::of(down)); 251 | } 252 | _ => (), 253 | } 254 | } else if let Some(Message::Tick) = envelope.message.downcast_ref::() { 255 | self.beat += 1; 256 | debug!("tag={} beat={} msg=Tick", sender.me(), self.beat); 257 | if self.beat % self.period == 0 { 258 | let events = self.detect(time); 259 | for e in events { 260 | warn!("\ttag={} event={:?}", sender.me(), e); 261 | } 262 | let gossip = self.gossip(time); 263 | info!("tag={} gossip: {:?}", sender.me(), gossip); 264 | let gossip: Vec = Message::Gossip(gossip).into(); 265 | self.peers.iter().for_each(|p| { 266 | let envelope = Envelope::of(gossip.clone()).from(sender.me()); 267 | sender.send(&p.tag, envelope); 268 | }); 269 | } 270 | let delay = Duration::from_millis(self.tick); 271 | sender.delay(sender.me(), Envelope::of(Message::Tick), delay); 272 | } else if let Some(Message::Init(horizon, tick, period, pings, countdown)) = 273 | envelope.message.downcast_ref::() 274 | { 275 | debug!( 276 | "tag={} msg=Init(horizon={} period={} tick={})", 277 | sender.me(), 278 | horizon, 279 | period, 280 | tick 281 | ); 282 | self.horizon = *horizon; 283 | self.tick = *tick; 284 | self.period = *period; 285 | self.countdown = countdown.to_owned(); 286 | sender.send(sender.me(), Envelope::of(Message::Tick)); 287 | 288 | pings.iter().for_each(|tag| { 289 | let ping: Vec = Message::Ping(tag.clone(), self.beat).into(); 290 | sender.send(tag, Envelope::of(ping).from(sender.me())); 291 | }) 292 | } 293 | } 294 | } 295 | 296 | #[derive(Default)] 297 | struct Countdown { 298 | n: usize, 299 | tx: Option>, 300 | } 301 | 302 | #[derive(Debug)] 303 | struct Setup(usize, Sender<()>); 304 | 305 | #[derive(Debug)] 306 | struct Down(String); 307 | 308 | impl From for Vec { 309 | fn from(down: Down) -> Self { 310 | down.0.into_bytes() 311 | } 312 | } 313 | 314 | impl From> for Down { 315 | fn from(vec: Vec) -> Self { 316 | Down(String::from_utf8(vec).unwrap()) 317 | } 318 | } 319 | 320 | impl AnyActor for Countdown { 321 | fn receive(&mut self, envelope: Envelope, sender: &mut dyn AnySender) { 322 | if let Some(Setup(n, tx)) = envelope.message.downcast_ref::() { 323 | self.n = *n; 324 | self.tx = Some(tx.to_owned()); 325 | } else if let Some(vec) = envelope.message.downcast_ref::>() { 326 | let Down(tag) = vec.to_owned().into(); 327 | warn!("countdown: down='{}'", tag); 328 | self.n -= 1; 329 | if self.n == 0 { 330 | self.tx.as_ref().unwrap().send(()).unwrap(); 331 | sender.stop(sender.me()); 332 | } 333 | } 334 | } 335 | } 336 | 337 | // RUST_LOG=warn cargo run --release --example gossip 338 | fn main() { 339 | env_logger::builder() 340 | .format_timestamp(Some(TimestampPrecision::Millis)) 341 | .init(); 342 | let pool = ThreadPool::new(6); 343 | 344 | let mut config = Config::default(); 345 | config.scheduler.actor_worker_threads = 1; 346 | config.scheduler.extra_worker_threads = 0; 347 | config.scheduler.logging_enabled = false; 348 | config.remote.enabled = true; 349 | 350 | let runs: Vec = { 351 | config.remote.listening = "127.0.0.1:9101".to_string(); 352 | let sys1 = System::new("gossip-1", "localhost", &config); 353 | let run1 = sys1.run(&pool).unwrap(); 354 | 355 | config.remote.listening = "127.0.0.1:9102".to_string(); 356 | let sys2 = System::new("gossip-2", "localhost", &config); 357 | let run2 = sys2.run(&pool).unwrap(); 358 | 359 | config.remote.listening = "127.0.0.1:9103".to_string(); 360 | let sys3 = System::new("gossip-3", "localhost", &config); 361 | let run3 = sys3.run(&pool).unwrap(); 362 | 363 | vec![run1, run2, run3] 364 | }; 365 | 366 | let r = runs.get(0).unwrap(); 367 | r.spawn_default::("countdown"); 368 | let (tx, rx) = bounded(1); 369 | r.send("countdown", Envelope::of(Setup(runs.len(), tx))); 370 | 371 | let (horizon, tick, period) = (1000, 100, 3); 372 | for (id, run) in runs.iter().enumerate() { 373 | let tag = format!("peer-{}", id); 374 | run.spawn_default::(&tag); 375 | let pings = if id == 0 { 376 | vec![] 377 | } else { 378 | vec!["peer-0@127.0.0.1:9101".to_string()] 379 | }; 380 | let init = Message::Init( 381 | horizon, 382 | period, 383 | tick, 384 | pings, 385 | "countdown@127.0.0.1:9101".to_string(), 386 | ); 387 | run.send(&tag, Envelope::of(init)); 388 | 389 | let millis = horizon * 5 * (1 + id as u64); 390 | let delay = Duration::from_millis(millis); 391 | let stop: Vec = Message::Stop.into(); 392 | run.delay(&tag, Envelope::of(stop), delay); 393 | } 394 | 395 | rx.recv().unwrap(); // wait until all 3 peers are down 396 | runs.into_iter().for_each(|run| run.shutdown()); 397 | } 398 | -------------------------------------------------------------------------------- /examples/hello.rs: -------------------------------------------------------------------------------- 1 | use std::sync::mpsc::{channel, Sender}; 2 | use std::time::Duration; 3 | 4 | extern crate uppercut; 5 | use uppercut::api::{AnyActor, AnySender, Envelope}; 6 | use uppercut::config::Config; 7 | use uppercut::core::System; 8 | use uppercut::pool::ThreadPool; 9 | 10 | #[derive(Debug, Clone)] 11 | struct Message(usize); 12 | 13 | struct State { 14 | tx: Sender, 15 | } 16 | 17 | impl State { 18 | fn new(tx: Sender) -> Self { 19 | Self { tx } 20 | } 21 | } 22 | 23 | impl AnyActor for State { 24 | fn receive(&mut self, envelope: Envelope, sender: &mut dyn AnySender) { 25 | if let Some(msg) = envelope.message.downcast_ref::() { 26 | sender.log(&format!("received: {:?}", msg)); 27 | 28 | if sender.me() == "copy" { 29 | self.tx.send(msg.0).unwrap(); 30 | } else { 31 | let tx = self.tx.clone(); 32 | sender.spawn("copy", Box::new(|| Box::new(State::new(tx)))); 33 | sender.send("copy", Envelope::of(msg.clone())); 34 | } 35 | sender.stop(sender.me()); 36 | } 37 | } 38 | } 39 | 40 | fn main() { 41 | // Total 6 threads: 42 | // = 1 scheduler thread (main event loop) 43 | // + 4 actor-worker threads (effective parallelism level) 44 | // + 1 background worker thread (logging, metrics, "housekeeping") 45 | let tp = ThreadPool::new(6); 46 | 47 | let cfg = Config::default(); 48 | let sys = System::new("basic", "localhost", &cfg); 49 | let run = sys.run(&tp).unwrap(); 50 | 51 | let (tx, rx) = channel(); 52 | run.spawn("state", || Box::new(State::new(tx))); 53 | run.send("state", Envelope::of(Message(42))); 54 | 55 | let timeout = Duration::from_secs(3); 56 | let result = rx.recv_timeout(timeout).unwrap(); 57 | println!("result: {}", result); 58 | run.shutdown(); 59 | } 60 | -------------------------------------------------------------------------------- /examples/paxos.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | use std::time::Duration; 3 | 4 | extern crate log; 5 | use env_logger::fmt::TimestampPrecision; 6 | use log::{debug, info}; 7 | 8 | extern crate bytes; 9 | use bytes::{Buf, BufMut, Bytes, BytesMut}; 10 | 11 | use crossbeam_channel::{bounded, Sender}; 12 | 13 | extern crate uppercut; 14 | use uppercut::api::{AnyActor, AnySender, Envelope}; 15 | use uppercut::config::Config; 16 | use uppercut::core::{Run, System}; 17 | use uppercut::pool::ThreadPool; 18 | 19 | const SEND_DELAY_MILLIS: u64 = 20; 20 | 21 | #[derive(Debug, Clone, Eq, PartialEq)] 22 | enum Message { 23 | Request { val: u64 }, 24 | Prepare { seq: u64 }, 25 | Promise { seq: u64 }, 26 | Ignored { seq: u64 }, 27 | Accept { seq: u64, val: u64 }, 28 | Accepted { seq: u64, val: u64 }, 29 | Rejected { seq: u64 }, 30 | Selected { seq: u64, val: u64 }, 31 | Empty, 32 | } 33 | 34 | impl From for Vec { 35 | fn from(message: Message) -> Self { 36 | let mut buf = BytesMut::with_capacity(1 + 8 + 8); 37 | match message { 38 | Message::Request { val } => { 39 | buf.put_u8(1); 40 | buf.put_u64(val); 41 | } 42 | Message::Prepare { seq } => { 43 | buf.put_u8(2); 44 | buf.put_u64(seq); 45 | } 46 | Message::Promise { seq } => { 47 | buf.put_u8(3); 48 | buf.put_u64(seq); 49 | } 50 | Message::Ignored { seq } => { 51 | buf.put_u8(4); 52 | buf.put_u64(seq); 53 | } 54 | Message::Accept { seq, val } => { 55 | buf.put_u8(5); 56 | buf.put_u64(seq); 57 | buf.put_u64(val); 58 | } 59 | Message::Accepted { seq, val } => { 60 | buf.put_u8(6); 61 | buf.put_u64(seq); 62 | buf.put_u64(val); 63 | } 64 | Message::Rejected { seq } => { 65 | buf.put_u8(7); 66 | buf.put_u64(seq); 67 | } 68 | Message::Selected { seq, val } => { 69 | buf.put_u8(8); 70 | buf.put_u64(seq); 71 | buf.put_u64(val); 72 | } 73 | Message::Empty => buf.put_u8(0), 74 | }; 75 | buf.split().to_vec() 76 | } 77 | } 78 | 79 | impl From> for Message { 80 | fn from(buf: Vec) -> Self { 81 | let mut buf = Bytes::from(buf); 82 | let op = buf.get_u8(); 83 | match op { 84 | 1 => Message::Request { val: buf.get_u64() }, 85 | 2 => Message::Prepare { seq: buf.get_u64() }, 86 | 3 => Message::Promise { seq: buf.get_u64() }, 87 | 4 => Message::Ignored { seq: buf.get_u64() }, 88 | 5 => Message::Accept { 89 | seq: buf.get_u64(), 90 | val: buf.get_u64(), 91 | }, 92 | 6 => Message::Accepted { 93 | seq: buf.get_u64(), 94 | val: buf.get_u64(), 95 | }, 96 | 7 => Message::Rejected { seq: buf.get_u64() }, 97 | 8 => Message::Selected { 98 | seq: buf.get_u64(), 99 | val: buf.get_u64(), 100 | }, 101 | _ => Message::Empty, 102 | } 103 | } 104 | } 105 | 106 | #[cfg(test)] 107 | mod tests { 108 | use super::*; 109 | 110 | #[test] 111 | fn test_into_from_bytes() { 112 | let messages = vec![ 113 | Message::Request { val: 42 }, 114 | Message::Prepare { seq: 42 }, 115 | Message::Promise { seq: 42 }, 116 | Message::Ignored { seq: 42 }, 117 | Message::Accept { 118 | seq: 42, 119 | val: 0xCAFEBABEDEADBEEF, 120 | }, 121 | Message::Accepted { 122 | seq: 42, 123 | val: 0xCAFEBABEDEADBEEF, 124 | }, 125 | Message::Rejected { seq: 42 }, 126 | Message::Selected { 127 | seq: 42, 128 | val: 0xCAFEBABEDEADBEEF, 129 | }, 130 | Message::Empty, 131 | ]; 132 | 133 | for message in messages { 134 | let buf: Vec = message.clone().into(); 135 | let msg: Message = buf.into(); 136 | assert_eq!(msg, message); 137 | } 138 | } 139 | } 140 | 141 | #[derive(Default)] 142 | struct Agent { 143 | me: String, 144 | peers: Vec, 145 | clients: HashSet, 146 | 147 | val: u64, 148 | seq: u64, 149 | seq_promised: u64, 150 | seq_accepted: u64, 151 | promised: HashSet, 152 | accepted: HashSet, 153 | storage: Vec<(u64, u64)>, 154 | } 155 | 156 | impl Agent { 157 | fn handle(&mut self, message: Message, from: String) -> Vec<(String, Message)> { 158 | match message { 159 | Message::Request { val } => { 160 | self.val = val; 161 | self.clients.insert(from); 162 | self.peers 163 | .iter() 164 | .map(|tag| (tag.clone(), Message::Prepare { seq: self.seq + 1 })) 165 | .collect() 166 | } 167 | Message::Prepare { seq } => { 168 | if seq > self.seq { 169 | self.seq = seq; 170 | vec![(from, Message::Promise { seq: self.seq })] 171 | } else { 172 | vec![(from, Message::Ignored { seq })] 173 | } 174 | } 175 | Message::Promise { seq } if seq > self.seq_promised => { 176 | self.promised.insert(from); 177 | if self.quorum(self.promised.len()) { 178 | self.seq_promised = seq; 179 | self.promised.clear(); 180 | let msg = Message::Accept { seq, val: self.val }; 181 | self.peers 182 | .iter() 183 | .map(|tag| (tag.clone(), msg.clone())) 184 | .collect() 185 | } else { 186 | vec![] 187 | } 188 | } 189 | Message::Ignored { seq: _ } => { 190 | vec![] 191 | } 192 | Message::Accept { seq, val } => { 193 | if seq >= self.seq { 194 | vec![(from, Message::Accepted { seq, val })] 195 | } else { 196 | vec![(from, Message::Rejected { seq })] 197 | } 198 | } 199 | Message::Accepted { seq, val } if seq > self.seq_accepted => { 200 | self.accepted.insert(from); 201 | if self.quorum(self.accepted.len()) { 202 | self.seq_accepted = seq; 203 | self.accepted.clear(); 204 | let msg = Message::Selected { seq, val }; 205 | self.peers 206 | .iter() 207 | .map(|tag| (tag.clone(), msg.clone())) 208 | .collect() 209 | } else { 210 | vec![] 211 | } 212 | } 213 | Message::Rejected { seq: _ } => { 214 | vec![] 215 | } 216 | Message::Selected { seq, val } => { 217 | self.storage.push((seq, val)); 218 | self.seq = seq; 219 | self.promised.clear(); 220 | self.clients 221 | .iter() 222 | .map(|tag| (tag.clone(), Message::Selected { seq, val })) 223 | .collect() 224 | } 225 | _ => vec![], 226 | } 227 | } 228 | 229 | fn quorum(&self, n: usize) -> bool { 230 | n > self.peers.len() / 2 231 | } 232 | } 233 | 234 | #[derive(Debug)] 235 | enum Control { 236 | Init(Vec), 237 | } 238 | 239 | impl AnyActor for Agent { 240 | fn receive(&mut self, envelope: Envelope, sender: &mut dyn AnySender) { 241 | if let Some(buf) = envelope.message.downcast_ref::>() { 242 | let msg: Message = buf.to_owned().into(); 243 | info!( 244 | "actor={} from={} message/parse={:?}", 245 | sender.me(), 246 | envelope.from, 247 | msg 248 | ); 249 | self.handle(msg, envelope.from) 250 | .into_iter() 251 | .for_each(|(target, msg)| { 252 | info!("\t{} sending to {}: {:?}", sender.me(), target, msg); 253 | let buf: Vec = msg.into(); 254 | let envelope = Envelope::of(buf).to(&target).from(sender.me()); 255 | let delay = Duration::from_millis(SEND_DELAY_MILLIS); 256 | sender.delay(&target, envelope, delay); 257 | }); 258 | } else if let Some(ctrl) = envelope.message.downcast_ref::() { 259 | match ctrl { 260 | Control::Init(peers) => { 261 | self.peers = peers.to_owned(); 262 | self.me = sender.me().to_string(); 263 | debug!( 264 | "tag={} init: peers={:?} me={}", 265 | sender.me(), 266 | self.peers, 267 | self.me 268 | ); 269 | } 270 | } 271 | } 272 | } 273 | } 274 | 275 | #[derive(Debug, Default)] 276 | struct Client { 277 | n: u32, 278 | id: u64, 279 | val: u64, 280 | done: bool, 281 | nodes: Vec, 282 | log: Vec, 283 | sender: Option)>>, 284 | } 285 | 286 | #[derive(Debug)] 287 | struct Setup(u32, u64, u64, Vec, Sender<(String, Vec)>); 288 | 289 | impl AnyActor for Client { 290 | fn receive(&mut self, envelope: Envelope, sender: &mut dyn AnySender) { 291 | if let Some(buf) = envelope.message.downcast_ref::>() { 292 | let message: Message = buf.to_owned().into(); 293 | info!("actor={} message={:?}", sender.me(), message); 294 | if let Message::Selected { seq: _, val } = message { 295 | self.log.push(val); 296 | self.done |= val == self.val; 297 | if !self.done { 298 | let idx: usize = self.id as usize % self.nodes.len(); 299 | let target = self.nodes.get(idx).unwrap(); 300 | let msg = Message::Request { val: self.val }; 301 | info!("actor={} retry/message={:?}", sender.me(), msg); 302 | let buf: Vec = msg.into(); 303 | let envelope = Envelope::of(buf).from(sender.me()); 304 | sender.send(target, envelope); 305 | } 306 | 307 | if self.log.len() == self.n as usize { 308 | self.sender 309 | .as_ref() 310 | .unwrap() 311 | .send((sender.me().to_string(), self.log.clone())) 312 | .unwrap() 313 | } 314 | } 315 | } else if let Some(setup) = envelope.message.downcast_ref::() { 316 | let Setup(n, id, val, nodes, tx) = setup; 317 | self.n = *n; 318 | self.id = *id; 319 | self.val = *val; 320 | self.done = false; 321 | self.nodes = nodes.to_owned(); 322 | self.sender = Some(tx.to_owned()); 323 | info!("actor={} val={} seq={:?}", sender.me(), self.val, self.log); 324 | 325 | let idx: usize = self.id as usize % self.nodes.len(); 326 | let target = self.nodes.get(idx).unwrap(); 327 | let msg = Message::Request { val: self.val }; 328 | let buf: Vec = msg.into(); 329 | let envelope = Envelope::of(buf).from(sender.me()); 330 | sender.send(target, envelope); 331 | } 332 | } 333 | } 334 | 335 | // RUST_LOG=info cargo run --release --example paxos 336 | fn main() { 337 | env_logger::builder() 338 | .format_timestamp(Some(TimestampPrecision::Millis)) 339 | .init(); 340 | let pool = ThreadPool::new(6); 341 | 342 | let mut config = Config::default(); 343 | config.scheduler.actor_worker_threads = 1; 344 | config.scheduler.extra_worker_threads = 0; 345 | config.remote.enabled = true; 346 | 347 | let runs: Vec = { 348 | config.remote.listening = "127.0.0.1:9001".to_string(); 349 | let sys1 = System::new("paxos-1", "localhost", &config); 350 | let run1 = sys1.run(&pool).unwrap(); 351 | 352 | config.remote.listening = "127.0.0.1:9002".to_string(); 353 | let sys2 = System::new("paxos-2", "localhost", &config); 354 | let run2 = sys2.run(&pool).unwrap(); 355 | 356 | config.remote.listening = "127.0.0.1:9003".to_string(); 357 | let sys3 = System::new("paxos-3", "localhost", &config); 358 | let run3 = sys3.run(&pool).unwrap(); 359 | 360 | vec![run1, run2, run3] 361 | }; 362 | 363 | const N: usize = 3; 364 | let peers: Vec = (0..N) 365 | .zip(9001..(9001 + N)) 366 | .into_iter() 367 | .map(|(i, port)| format!("node-{}@127.0.0.1:{}", i, port)) 368 | .collect(); 369 | 370 | for (address, run) in peers.iter().zip(runs.iter()) { 371 | let tag = address.split('@').next().unwrap(); 372 | run.spawn_default::(tag); 373 | let envelope = Envelope::of(Control::Init(peers.clone())); 374 | run.send(tag, envelope); 375 | } 376 | 377 | let clients = vec![("client-A", 30), ("client-B", 73), ("client-C", 42)]; 378 | let (tx, rx) = bounded(clients.len()); 379 | 380 | for ((id, (tag, val)), run) in clients.clone().into_iter().enumerate().zip(runs.iter()) { 381 | run.spawn_default::(tag); 382 | let setup = Setup( 383 | clients.len() as u32, 384 | id as u64, 385 | val, 386 | peers.clone(), 387 | tx.clone(), 388 | ); 389 | let envelope = Envelope::of(setup); 390 | run.send(tag, envelope); 391 | println!("{}: {}", tag, val); 392 | } 393 | 394 | let mut seen: HashSet> = HashSet::with_capacity(clients.len() + 1); 395 | for _ in 0..clients.len() { 396 | if let Ok(received) = rx.recv_timeout(Duration::from_secs(20)) { 397 | println!("{:?}", received); 398 | seen.insert(received.1); 399 | } else { 400 | break; 401 | } 402 | } 403 | 404 | runs.into_iter().for_each(|run| run.shutdown()); 405 | 406 | let ok = { 407 | let numbers: Vec = clients.iter().map(|(_, n)| *n).collect(); 408 | seen.iter() 409 | .all(|vec| numbers.iter().all(|x| vec.contains(x))) 410 | }; 411 | if seen.len() == 1 && ok { 412 | println!("OK"); 413 | std::process::exit(0); 414 | } else { 415 | println!("FAILED"); 416 | std::process::exit(1); 417 | } 418 | } 419 | -------------------------------------------------------------------------------- /examples/pi.rs: -------------------------------------------------------------------------------- 1 | use std::sync::mpsc::{channel, Sender}; 2 | 3 | extern crate uppercut; 4 | use uppercut::api::{AnyActor, AnySender, Envelope}; 5 | use uppercut::config::Config; 6 | use uppercut::core::System; 7 | use uppercut::pool::ThreadPool; 8 | 9 | extern crate num_cpus; 10 | 11 | extern crate rand; 12 | use rand::Rng; 13 | use std::time::Instant; 14 | 15 | #[derive(Default)] 16 | struct Master { 17 | size: usize, 18 | hits: usize, 19 | total: usize, 20 | result: Option>, 21 | } 22 | 23 | #[derive(Debug)] 24 | struct Pi { 25 | workers: usize, 26 | throws: usize, 27 | result: Sender, 28 | } 29 | 30 | #[derive(Default)] 31 | struct Worker; 32 | 33 | #[derive(Debug)] 34 | struct Task(usize); 35 | 36 | #[derive(Debug)] 37 | struct Done(usize, usize); 38 | 39 | impl AnyActor for Master { 40 | fn receive(&mut self, envelope: Envelope, sender: &mut dyn AnySender) { 41 | if let Some(Pi { 42 | workers, 43 | throws, 44 | result, 45 | }) = envelope.message.downcast_ref::() 46 | { 47 | self.size = *workers; 48 | self.result = Some(result.clone()); 49 | for idx in 0..self.size { 50 | let id = format!("worker-{}", idx); 51 | sender.spawn(&id, Box::new(|| Box::new(Worker::default()))); 52 | let task = Envelope::of(Task(*throws)).from(sender.me()); 53 | sender.send(&id, task); 54 | } 55 | } else if let Some(Done(hits, total)) = envelope.message.downcast_ref::() { 56 | self.size -= 1; 57 | self.hits += hits; 58 | self.total += total; 59 | 60 | if self.size == 0 { 61 | let pi = 4.0 * self.hits as f64 / self.total as f64; 62 | self.result 63 | .as_ref() 64 | .iter() 65 | .for_each(|tx| tx.send(pi).unwrap()); 66 | } 67 | } 68 | } 69 | } 70 | 71 | impl AnyActor for Worker { 72 | fn receive(&mut self, envelope: Envelope, sender: &mut dyn AnySender) { 73 | if let Some(Task(size)) = envelope.message.downcast_ref::() { 74 | let mut hits: usize = 0; 75 | let mut rng = rand::thread_rng(); 76 | for _ in 0..*size { 77 | let x: f64 = rng.gen_range(-1.0, 1.0); 78 | let y: f64 = rng.gen_range(-1.0, 1.0); 79 | 80 | if (x * x + y * y).sqrt() <= 1.0 { 81 | hits += 1; 82 | } 83 | } 84 | let done = Envelope::of(Done(hits, *size)).from(sender.me()); 85 | sender.send(&envelope.from, done); 86 | sender.stop(sender.me()); 87 | } 88 | } 89 | } 90 | 91 | fn main() -> Result<(), Box> { 92 | let cores = num_cpus::get(); 93 | let pool = ThreadPool::new(cores + 2); // +1 event loop, +1 worker thread 94 | 95 | let cfg = Config::default(); 96 | let sys = System::new("pi", "localhost", &cfg); 97 | let run = sys.run(&pool).unwrap(); 98 | 99 | let now = Instant::now(); 100 | let (tx, rx) = channel(); 101 | run.spawn_default::("master"); 102 | let pi = Pi { 103 | workers: 10000, 104 | throws: 100000, 105 | result: tx, 106 | }; 107 | println!( 108 | "Submitting {} workers making {} throws each.", 109 | pi.workers, pi.throws 110 | ); 111 | let envelope = Envelope::of(pi); 112 | run.send("master", envelope); 113 | 114 | let pi = rx.recv().unwrap(); 115 | let seconds = now.elapsed().as_secs(); 116 | println!("Pi estimate: {} (in {} seconds)", pi, seconds); 117 | 118 | run.shutdown(); 119 | Ok(()) 120 | } 121 | -------------------------------------------------------------------------------- /examples/ping.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use uppercut::api::{AnyActor, AnySender, Envelope}; 4 | use uppercut::config::{ClientConfig, Config, RemoteConfig, SchedulerConfig, ServerConfig}; 5 | use uppercut::core::System; 6 | use uppercut::pool::ThreadPool; 7 | 8 | extern crate clap; 9 | use clap::{App, Arg}; 10 | 11 | #[derive(Default)] 12 | struct PingPong; 13 | 14 | impl AnyActor for PingPong { 15 | fn receive(&mut self, envelope: Envelope, sender: &mut dyn AnySender) { 16 | if let Some(vec) = envelope.message.downcast_ref::>() { 17 | let msg = String::from_utf8(vec.clone()).unwrap(); 18 | println!("PingPong: actor '{}' received '{}'\n", sender.me(), msg); 19 | 20 | let response = if vec == b"ping" { 21 | Envelope::of(b"pong".to_vec()).from(sender.me()) 22 | } else { 23 | // vec == b"pong" 24 | Envelope::of(b"ping".to_vec()).from(sender.me()) 25 | }; 26 | 27 | sender.delay(&envelope.from, response, Duration::from_secs(1)); 28 | } 29 | } 30 | } 31 | 32 | // cargo run --example ping -- --listen 0.0.0.0:10001 33 | // cargo run --example ping -- --listen 0.0.0.0:10002 --peer ping@127.0.0.1:10001 34 | fn main() { 35 | let matches = App::new("pingpong") 36 | .arg( 37 | Arg::with_name("listen") 38 | .long("listen") 39 | .short("l") 40 | .required(true) 41 | .takes_value(true), 42 | ) 43 | .arg( 44 | Arg::with_name("peer") 45 | .long("peer") 46 | .short("p") 47 | .required(false) 48 | .takes_value(true), 49 | ) 50 | .get_matches(); 51 | 52 | let listen = matches.value_of("listen").expect("'listen' is missing"); 53 | let peer = matches.value_of("peer"); 54 | 55 | let cores = std::cmp::max(4, num_cpus::get()); 56 | let pool = ThreadPool::new(cores + 2 + 1); 57 | 58 | let cfg = Config::new( 59 | SchedulerConfig::with_total_threads(cores / 2), 60 | RemoteConfig::listening_at(listen, ServerConfig::default(), ClientConfig::default()), 61 | ); 62 | let sys = System::new("one", "localhost", &cfg); 63 | let run = sys.run(&pool).unwrap(); 64 | 65 | run.spawn_default::("ping"); 66 | 67 | if let Some(addr) = peer { 68 | let env = Envelope::of(b"ping".to_vec()).from("ping"); 69 | run.send(addr, env); 70 | } 71 | 72 | std::thread::park(); // block current thread 73 | } 74 | -------------------------------------------------------------------------------- /examples/remote.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use uppercut::api::{AnyActor, AnySender, Envelope}; 4 | use uppercut::config::{ClientConfig, Config, RemoteConfig, SchedulerConfig, ServerConfig}; 5 | use uppercut::core::System; 6 | use uppercut::pool::ThreadPool; 7 | 8 | #[derive(Default)] 9 | struct PingPong; 10 | 11 | impl AnyActor for PingPong { 12 | fn receive(&mut self, envelope: Envelope, sender: &mut dyn AnySender) { 13 | if let Some(vec) = envelope.message.downcast_ref::>() { 14 | let msg = String::from_utf8(vec.clone()).unwrap(); 15 | sender.log(&format!("received '{}'", msg)); 16 | 17 | let response = if vec == b"ping" { 18 | Envelope::of(b"pong".to_vec()).from(sender.me()) 19 | } else { 20 | // vec == b"pong" 21 | Envelope::of(b"ping".to_vec()).from(sender.me()) 22 | }; 23 | 24 | sender.delay(&envelope.from, response, Duration::from_secs(1)); 25 | } 26 | } 27 | } 28 | 29 | // cargo run --release --example remote 30 | fn main() { 31 | let cores = std::cmp::max(4, num_cpus::get()); 32 | let pool = ThreadPool::new(cores + 2 + 1); 33 | 34 | // sys 1 35 | 36 | let cfg1 = Config::new( 37 | SchedulerConfig::with_total_threads(cores / 2), 38 | RemoteConfig::listening_at( 39 | "0.0.0.0:10001", 40 | ServerConfig::default(), 41 | ClientConfig::default(), 42 | ), 43 | ); 44 | let sys1 = System::new("one", "localhost", &cfg1); 45 | let run1 = sys1.run(&pool).unwrap(); 46 | 47 | run1.spawn_default::("ping"); 48 | 49 | // sys 2 50 | 51 | let cfg2 = Config::new( 52 | SchedulerConfig::with_total_threads(cores / 2), 53 | RemoteConfig::listening_at( 54 | "0.0.0.0:10002", 55 | ServerConfig::default(), 56 | ClientConfig::default(), 57 | ), 58 | ); 59 | let sys2 = System::new("two", "localhost", &cfg2); 60 | let run2 = sys2.run(&pool).unwrap(); 61 | 62 | run2.spawn_default::("pong"); 63 | 64 | // send initial ping 65 | 66 | let env1 = Envelope::of(b"pong".to_vec()).from("ping"); 67 | run1.send("pong@127.0.0.1:10002", env1); 68 | 69 | std::thread::park(); // block current thread 70 | } 71 | -------------------------------------------------------------------------------- /examples/stop.rs: -------------------------------------------------------------------------------- 1 | use std::sync::mpsc::{channel, Sender}; 2 | 3 | extern crate uppercut; 4 | use std::any::Any; 5 | use std::thread::sleep; 6 | use std::time::Duration; 7 | use uppercut::api::{AnyActor, AnySender, Envelope}; 8 | use uppercut::config::{Config, SchedulerConfig}; 9 | use uppercut::core::System; 10 | use uppercut::pool::ThreadPool; 11 | 12 | #[derive(Debug)] 13 | struct Message(Option, Sender); 14 | 15 | #[derive(Default)] 16 | struct State; 17 | 18 | impl AnyActor for State { 19 | fn receive(&mut self, envelope: Envelope, sender: &mut dyn AnySender) { 20 | if let Some(msg) = envelope.message.downcast_ref::() { 21 | sender.log(&format!("received: {:?}", msg)); 22 | let x = msg.0.unwrap(); 23 | msg.1.send(x).unwrap(); 24 | } 25 | } 26 | 27 | fn on_fail(&self, _error: Box, sender: &mut dyn AnySender) { 28 | sender.log("failure detected!"); 29 | } 30 | 31 | fn on_stop(&self, sender: &mut dyn AnySender) { 32 | sender.log("shutting down"); 33 | } 34 | } 35 | 36 | fn main() { 37 | let cfg = Config::new( 38 | SchedulerConfig { 39 | eager_shutdown_enabled: false, 40 | ..Default::default() 41 | }, 42 | Default::default(), 43 | ); 44 | let sys = System::new("stop-example", "localhost", &cfg); 45 | let pool = ThreadPool::new(6); 46 | let run = sys.run(&pool).unwrap(); 47 | run.spawn_default::("x"); 48 | 49 | let (tx, rx) = channel(); 50 | run.send("x", Envelope::of(Message(Some(42), tx.clone()))); 51 | run.stop("x"); 52 | run.send("x", Envelope::of(Message(Some(100500), tx))); 53 | 54 | println!("recv: {}", rx.recv().unwrap()); 55 | sleep(Duration::from_secs(3)); 56 | run.shutdown(); 57 | } 58 | -------------------------------------------------------------------------------- /src/api.rs: -------------------------------------------------------------------------------- 1 | use std::any::Any; 2 | use std::fmt::Debug; 3 | use std::time::{Duration, SystemTime}; 4 | 5 | pub type Actor = Box; 6 | 7 | pub type Message = Box; 8 | 9 | pub trait AnyActor: Send { 10 | fn receive(&mut self, envelope: Envelope, sender: &mut dyn AnySender); 11 | fn on_fail(&self, _error: Box, _sender: &mut dyn AnySender) {} 12 | fn on_stop(&self, _sender: &mut dyn AnySender) {} 13 | } 14 | 15 | pub trait AnySender { 16 | fn me(&self) -> &str; 17 | fn send(&self, address: &str, envelope: Envelope); 18 | fn spawn(&self, address: &str, f: Box Actor>); 19 | fn delay(&self, address: &str, envelope: Envelope, duration: Duration); 20 | fn stop(&self, address: &str); 21 | fn log(&mut self, message: &str); 22 | fn metric(&mut self, name: &str, value: f64); 23 | fn now(&self) -> SystemTime; 24 | } 25 | 26 | #[derive(Debug)] 27 | pub struct Envelope { 28 | pub message: Message, 29 | pub from: String, 30 | pub to: String, 31 | } 32 | 33 | impl Envelope { 34 | pub fn of(message: T) -> Envelope { 35 | Envelope { 36 | message: Box::new(message), 37 | from: String::default(), 38 | to: String::default(), 39 | } 40 | } 41 | 42 | pub fn from(mut self, from: &str) -> Self { 43 | self.from = from.to_string(); 44 | self 45 | } 46 | 47 | pub fn to(mut self, to: &str) -> Self { 48 | self.to = to.to_string(); 49 | self 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | #[derive(Default, Clone)] 4 | pub struct Config { 5 | pub scheduler: SchedulerConfig, 6 | pub remote: RemoteConfig, 7 | } 8 | 9 | impl Config { 10 | pub fn new(scheduler: SchedulerConfig, remote: RemoteConfig) -> Config { 11 | Config { scheduler, remote } 12 | } 13 | } 14 | 15 | #[derive(Debug, Clone)] 16 | pub struct ServerConfig { 17 | pub events_capacity: usize, 18 | pub recv_buffer_size: usize, 19 | pub send_buffer_size: usize, 20 | } 21 | 22 | impl Default for ServerConfig { 23 | fn default() -> Self { 24 | Self { 25 | events_capacity: 1024, 26 | recv_buffer_size: 1024, 27 | send_buffer_size: 1024, 28 | } 29 | } 30 | } 31 | 32 | #[derive(Debug, Clone)] 33 | pub struct ClientConfig { 34 | pub events_capacity: usize, 35 | pub recv_buffer_size: usize, 36 | pub send_buffer_size: usize, 37 | } 38 | 39 | impl Default for ClientConfig { 40 | fn default() -> Self { 41 | Self { 42 | events_capacity: 1024, 43 | recv_buffer_size: 1024, 44 | send_buffer_size: 1024, 45 | } 46 | } 47 | } 48 | 49 | #[derive(Clone, Default)] 50 | pub struct RemoteConfig { 51 | pub enabled: bool, 52 | pub listening: String, 53 | pub server: ServerConfig, 54 | pub client: ClientConfig, 55 | } 56 | 57 | impl RemoteConfig { 58 | pub fn listening_at(listening: &str, server: ServerConfig, client: ClientConfig) -> Self { 59 | Self { 60 | enabled: true, 61 | listening: listening.to_string(), 62 | server, 63 | client, 64 | } 65 | } 66 | } 67 | 68 | #[derive(Clone)] 69 | pub struct SchedulerConfig { 70 | pub default_mailbox_capacity: usize, 71 | pub logging_enabled: bool, 72 | pub metric_reporting_enabled: bool, 73 | pub metric_reporting_interval: Duration, 74 | pub delay_precision: Duration, 75 | pub actor_worker_threads: usize, 76 | pub extra_worker_threads: usize, 77 | pub eager_shutdown_enabled: bool, 78 | } 79 | 80 | impl Default for SchedulerConfig { 81 | fn default() -> Self { 82 | SchedulerConfig { 83 | default_mailbox_capacity: 16, 84 | logging_enabled: true, 85 | metric_reporting_enabled: false, 86 | metric_reporting_interval: Duration::from_secs(1), 87 | delay_precision: Duration::from_millis(1), 88 | actor_worker_threads: 4, 89 | extra_worker_threads: 1, 90 | eager_shutdown_enabled: true, 91 | } 92 | } 93 | } 94 | 95 | impl SchedulerConfig { 96 | pub fn with_total_threads(total_threads: usize) -> Self { 97 | SchedulerConfig { 98 | actor_worker_threads: total_threads, 99 | ..Default::default() 100 | } 101 | } 102 | 103 | pub(crate) fn total_threads_required(&self) -> usize { 104 | self.actor_worker_threads + self.extra_worker_threads + 1 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/core.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque}; 3 | use std::ops::Add; 4 | use std::panic::{self, AssertUnwindSafe}; 5 | use std::time::{Duration, Instant, SystemTime}; 6 | 7 | use crossbeam_channel::{unbounded, Receiver, SendError, Sender}; 8 | 9 | use crate::api::{Actor, AnyActor, AnySender, Envelope}; 10 | use crate::config::{Config, SchedulerConfig}; 11 | use crate::error::Error; 12 | use crate::monitor::{LoggerEntry, Meta, MetricEntry, SchedulerMetrics}; 13 | use crate::pool::{Runnable, ThreadPool}; 14 | 15 | use crate::remote::client::{self, Client}; 16 | use crate::remote::server::{self, Server}; 17 | 18 | const CLIENT: &str = "$CLIENT"; 19 | const SERVER: &str = "$SERVER"; 20 | 21 | impl AnySender for Local { 22 | fn me(&self) -> &str { 23 | &self.tag 24 | } 25 | 26 | fn send(&self, address: &str, mut envelope: Envelope) { 27 | let tag = adjust_remote_address(address, &mut envelope).to_string(); 28 | let action = Action::Queue { tag, envelope }; 29 | self.tx.send(action).unwrap(); 30 | } 31 | 32 | fn spawn(&self, address: &str, f: Box Actor>) { 33 | let action = Action::Spawn { 34 | tag: address.to_string(), 35 | actor: f(), 36 | }; 37 | self.tx.send(action).unwrap(); 38 | } 39 | 40 | fn delay(&self, address: &str, mut envelope: Envelope, duration: Duration) { 41 | let at = Instant::now().add(duration); 42 | let tag = adjust_remote_address(address, &mut envelope).to_string(); 43 | let entry = Entry { at, tag, envelope }; 44 | let action = Action::Delay { entry }; 45 | self.tx.send(action).unwrap(); 46 | } 47 | 48 | fn stop(&self, address: &str) { 49 | let action = Action::Stop { 50 | tag: address.to_string(), 51 | }; 52 | self.tx.send(action).unwrap(); 53 | } 54 | 55 | fn log(&mut self, message: &str) { 56 | self.logs.push((self.now(), message.to_string())); 57 | } 58 | 59 | fn metric(&mut self, name: &str, value: f64) { 60 | let now = self.now(); 61 | self.metrics 62 | .entry(name.to_string()) 63 | .or_default() 64 | .push((now, value)); 65 | } 66 | 67 | fn now(&self) -> SystemTime { 68 | SystemTime::now() 69 | } 70 | } 71 | 72 | impl Local { 73 | fn drain(&mut self, tx: &Sender) -> Result<(), SendError> { 74 | if !self.logs.is_empty() { 75 | let action = Action::Logs { 76 | tag: self.tag.clone(), 77 | logs: self.logs.to_owned(), 78 | }; 79 | tx.send(action)?; 80 | self.logs.clear(); 81 | } 82 | if !self.metrics.is_empty() { 83 | let action = Action::Metrics { 84 | map: self.metrics.to_owned(), 85 | }; 86 | tx.send(action)?; 87 | self.metrics.clear(); 88 | } 89 | Ok(()) 90 | } 91 | } 92 | 93 | struct Local { 94 | tx: Sender, 95 | tag: String, 96 | logs: Vec<(SystemTime, String)>, 97 | metrics: HashMap>, 98 | } 99 | 100 | impl Local { 101 | fn new(tx: Sender) -> Self { 102 | Self { 103 | tx, 104 | tag: Default::default(), 105 | logs: Default::default(), 106 | metrics: Default::default(), 107 | } 108 | } 109 | } 110 | 111 | struct Scheduler { 112 | config: SchedulerConfig, 113 | actors: HashMap, 114 | queue: HashMap>, 115 | tasks: BinaryHeap, 116 | active: HashSet, 117 | } 118 | 119 | impl Scheduler { 120 | fn with_config(config: &SchedulerConfig) -> Scheduler { 121 | Scheduler { 122 | config: config.clone(), 123 | actors: HashMap::default(), 124 | queue: HashMap::default(), 125 | tasks: BinaryHeap::default(), 126 | active: HashSet::default(), 127 | } 128 | } 129 | } 130 | 131 | // received by Worker threads 132 | enum Event { 133 | Mail { 134 | tag: String, 135 | actor: Actor, 136 | envelope: Envelope, 137 | }, 138 | Stop { 139 | tag: String, 140 | actor: Actor, 141 | }, 142 | Shutdown, 143 | } 144 | 145 | // received by the Scheduler thread 146 | enum Action { 147 | Return { 148 | tag: String, 149 | actor: Actor, 150 | ok: bool, 151 | }, 152 | Spawn { 153 | tag: String, 154 | actor: Actor, 155 | }, 156 | Queue { 157 | tag: String, 158 | envelope: Envelope, 159 | }, 160 | Delay { 161 | entry: Entry, 162 | }, 163 | Stop { 164 | tag: String, 165 | }, 166 | Logs { 167 | tag: String, 168 | logs: Vec<(SystemTime, String)>, 169 | }, 170 | Metrics { 171 | map: HashMap>, 172 | }, 173 | Shutdown, 174 | } 175 | 176 | struct Entry { 177 | at: Instant, 178 | tag: String, 179 | envelope: Envelope, 180 | } 181 | 182 | impl Eq for Entry {} 183 | 184 | impl PartialEq for Entry { 185 | fn eq(&self, other: &Self) -> bool { 186 | self.at == other.at 187 | } 188 | } 189 | 190 | impl Ord for Entry { 191 | fn cmp(&self, other: &Self) -> Ordering { 192 | // reverse ordering for min-heap 193 | self.at.cmp(&other.at).reverse() 194 | } 195 | } 196 | 197 | impl PartialOrd for Entry { 198 | fn partial_cmp(&self, other: &Self) -> Option { 199 | Some(self.cmp(other)) 200 | } 201 | } 202 | 203 | struct Runtime<'a> { 204 | name: String, 205 | host: String, 206 | pool: &'a ThreadPool, 207 | config: Config, 208 | } 209 | 210 | impl<'a> Runtime<'a> { 211 | fn new(name: String, host: String, pool: &'a ThreadPool, config: Config) -> Runtime { 212 | Runtime { 213 | name, 214 | host, 215 | pool, 216 | config, 217 | } 218 | } 219 | 220 | fn start(self) -> Result, Error> { 221 | let (pool, config) = (self.pool, self.config); 222 | let events = unbounded(); 223 | let actions = unbounded(); 224 | let sender = actions.0.clone(); 225 | 226 | start_actor_runtime( 227 | self.name, 228 | self.host, 229 | pool, 230 | config.scheduler, 231 | events, 232 | actions, 233 | ); 234 | let run = Run { sender, pool }; 235 | 236 | if config.remote.enabled { 237 | let server = Server::listen(&config.remote.listening, &config.remote.server)?; 238 | let port = server.port(); 239 | run.spawn(SERVER, move || Box::new(server)); 240 | run.send(SERVER, Envelope::of(server::Loop)); 241 | let client = Client::new(port, &config.remote.client); 242 | run.spawn(CLIENT, move || Box::new(client)); 243 | run.send(CLIENT, Envelope::of(client::Loop)); 244 | } 245 | 246 | Ok(run) 247 | } 248 | } 249 | 250 | #[derive(Default)] 251 | pub struct System { 252 | name: String, 253 | host: String, 254 | config: Config, 255 | } 256 | 257 | impl System { 258 | pub fn new(name: &str, host: &str, config: &Config) -> System { 259 | System { 260 | name: name.to_string(), 261 | host: host.to_string(), 262 | config: config.clone(), 263 | } 264 | } 265 | 266 | pub fn run(self, pool: &ThreadPool) -> Result { 267 | if pool.size() < self.config.scheduler.total_threads_required() { 268 | Err(Error::ThreadPoolTooSmall { 269 | required: self.config.scheduler.total_threads_required(), 270 | available: pool.size(), 271 | }) 272 | } else { 273 | let runtime = Runtime::new(self.name, self.host, pool, self.config); 274 | Ok(runtime.start()?) 275 | } 276 | } 277 | } 278 | 279 | pub struct Run<'a> { 280 | pool: &'a ThreadPool, 281 | sender: Sender, 282 | } 283 | 284 | impl<'a> Run<'a> { 285 | pub fn send(&self, address: &str, mut envelope: Envelope) { 286 | let tag = adjust_remote_address(address, &mut envelope).to_string(); 287 | let action = Action::Queue { tag, envelope }; 288 | self.sender.send(action).unwrap(); 289 | } 290 | 291 | pub fn spawn Actor>(&self, address: &str, f: F) { 292 | let action = Action::Spawn { 293 | tag: address.to_string(), 294 | actor: f(), 295 | }; 296 | self.sender.send(action).unwrap(); 297 | } 298 | 299 | pub fn spawn_default(&self, address: &str) { 300 | let action = Action::Spawn { 301 | tag: address.to_string(), 302 | actor: Box::::default(), 303 | }; 304 | self.sender.send(action).unwrap(); 305 | } 306 | 307 | pub fn delay(&self, address: &str, mut envelope: Envelope, duration: Duration) { 308 | let at = Instant::now().add(duration); 309 | let tag = adjust_remote_address(address, &mut envelope).to_string(); 310 | let entry = Entry { at, tag, envelope }; 311 | let action = Action::Delay { entry }; 312 | self.sender.send(action).unwrap(); 313 | } 314 | 315 | pub fn stop(&self, address: &str) { 316 | let action = Action::Stop { 317 | tag: address.to_string(), 318 | }; 319 | self.sender.send(action).unwrap(); 320 | } 321 | 322 | pub fn shutdown(self) { 323 | let action = Action::Shutdown; 324 | let _ = self.sender.send(action); 325 | } 326 | 327 | pub fn submit(&self, f: F) { 328 | self.pool.submit(f); 329 | } 330 | } 331 | 332 | fn worker_loop(tx: Sender, rx: Receiver) { 333 | let mut sender = Local::new(tx.clone()); 334 | loop { 335 | let event = rx.try_recv(); 336 | if let Ok(e) = event { 337 | match e { 338 | Event::Mail { 339 | tag, 340 | mut actor, 341 | envelope, 342 | } => { 343 | sender.tag = tag.clone(); 344 | let result = panic::catch_unwind(AssertUnwindSafe(|| { 345 | actor.receive(envelope, &mut sender); 346 | })); 347 | let ok = result.is_ok(); 348 | if !ok { 349 | actor.on_fail(result.err().unwrap(), &mut sender); 350 | } 351 | let sent = tx.send(Action::Return { tag, actor, ok }); 352 | if sent.is_err() { 353 | break; 354 | } 355 | } 356 | Event::Stop { tag, actor } => { 357 | sender.tag = tag; 358 | actor.on_stop(&mut sender); 359 | } 360 | Event::Shutdown => break, 361 | } 362 | sender.drain(&tx).unwrap(); 363 | } 364 | } 365 | } 366 | 367 | fn event_loop( 368 | actions_rx: Receiver, 369 | actions_tx: Sender, 370 | events_tx: Sender, 371 | mut scheduler: Scheduler, 372 | background: impl Fn(Runnable), 373 | name: String, 374 | host: String, 375 | ) { 376 | let mut scheduler_metrics = SchedulerMetrics::named(name.clone()); 377 | let mut start = Instant::now(); 378 | let mut logs = Vec::with_capacity(1024); 379 | let mut metrics = HashMap::with_capacity(1024); 380 | 381 | let min_timeout_millis: u64 = 1; 382 | let max_timeout_millis: u64 = 256; 383 | let mut timeout_millis = max_timeout_millis; 384 | 'main: loop { 385 | let received = actions_rx.recv_timeout(Duration::from_millis(timeout_millis)); 386 | if let Ok(action) = received { 387 | timeout_millis = std::cmp::max(min_timeout_millis, timeout_millis / 2); 388 | scheduler_metrics.hit += 1; 389 | match action { 390 | Action::Return { tag, actor, ok } if scheduler.active.contains(&tag) => { 391 | if !ok { 392 | scheduler_metrics.failures += 1; 393 | scheduler.active.remove(&tag); 394 | scheduler.queue.remove(&tag); 395 | continue 'main; 396 | } 397 | scheduler_metrics.returns += 1; 398 | 399 | if let Some(envelope) = scheduler.queue.get_mut(&tag).unwrap().pop_front() { 400 | let event = Event::Mail { 401 | tag, 402 | actor, 403 | envelope, 404 | }; 405 | events_tx.send(event).unwrap(); 406 | } else { 407 | scheduler.actors.insert(tag, actor); 408 | } 409 | } 410 | Action::Return { tag, actor, ok } => { 411 | // Returned actor was stopped before (removed from active set). 412 | scheduler.queue.remove(&tag); 413 | if ok { 414 | let event = Event::Stop { tag, actor }; 415 | events_tx.send(event).unwrap(); 416 | } 417 | } 418 | Action::Queue { tag, envelope } if scheduler.active.contains(&tag) => { 419 | scheduler_metrics.queues += 1; 420 | scheduler_metrics.messages += 1; 421 | scheduler.queue.get_mut(&tag).unwrap().push_back(envelope); 422 | if let Some(actor) = scheduler.actors.remove(&tag) { 423 | let envelope = scheduler.queue.get_mut(&tag).unwrap().pop_front().unwrap(); 424 | let event = Event::Mail { 425 | tag, 426 | actor, 427 | envelope, 428 | }; 429 | events_tx.send(event).unwrap(); 430 | } 431 | } 432 | Action::Spawn { tag, actor } if !scheduler.active.contains(&tag) => { 433 | scheduler_metrics.spawns += 1; 434 | scheduler.active.insert(tag.clone()); 435 | scheduler.actors.insert(tag.clone(), actor); 436 | scheduler.queue.insert( 437 | tag.clone(), 438 | VecDeque::with_capacity(scheduler.config.default_mailbox_capacity), 439 | ); 440 | } 441 | Action::Delay { entry } => { 442 | scheduler_metrics.delays += 1; 443 | scheduler.tasks.push(entry); 444 | } 445 | Action::Stop { tag } if scheduler.active.contains(&tag) => { 446 | scheduler_metrics.stops += 1; 447 | scheduler.active.remove(&tag); 448 | scheduler.queue.remove(&tag); 449 | if scheduler.actors.contains_key(&tag) { 450 | let actor = scheduler.actors.remove(&tag).unwrap(); 451 | let event = Event::Stop { tag, actor }; 452 | events_tx.send(event).unwrap(); 453 | } 454 | } 455 | Action::Logs { tag, logs: entries } => { 456 | logs.push((tag, entries)); 457 | } 458 | Action::Metrics { map } => { 459 | for (name, mut entries) in map { 460 | metrics 461 | .entry(name) 462 | .or_insert_with(Vec::default) 463 | .append(&mut entries); 464 | } 465 | } 466 | Action::Shutdown => break 'main, 467 | _ => { 468 | scheduler_metrics.drops += 1; 469 | } 470 | } 471 | } else { 472 | timeout_millis = std::cmp::min(timeout_millis * 2, max_timeout_millis); 473 | scheduler_metrics.miss += 1; 474 | } 475 | 476 | let now = Instant::now().add(scheduler.config.delay_precision / 2); 477 | while scheduler 478 | .tasks 479 | .peek() 480 | .map(|e| e.at <= now) 481 | .unwrap_or_default() 482 | { 483 | if let Some(Entry { tag, envelope, .. }) = scheduler.tasks.pop() { 484 | let action = Action::Queue { tag, envelope }; 485 | actions_tx.send(action).unwrap(); 486 | } 487 | } 488 | 489 | scheduler_metrics.ticks += 1; 490 | if start.elapsed() >= scheduler.config.metric_reporting_interval { 491 | let now = SystemTime::now(); 492 | scheduler_metrics.at = now 493 | .duration_since(SystemTime::UNIX_EPOCH) 494 | .unwrap() 495 | .as_millis() as u64; 496 | scheduler_metrics.actors = scheduler.active.len() as u64; 497 | 498 | if scheduler.config.metric_reporting_enabled { 499 | report_metrics( 500 | &background, 501 | &name, 502 | &host, 503 | scheduler_metrics.clone(), 504 | metrics, 505 | ); 506 | scheduler_metrics.reset(); 507 | metrics = HashMap::with_capacity(1024); 508 | } 509 | 510 | if scheduler.config.logging_enabled { 511 | report_logs(&background, &name, &host, logs); 512 | logs = Vec::with_capacity(1024); 513 | } 514 | 515 | start = Instant::now(); 516 | } 517 | 518 | if scheduler.config.eager_shutdown_enabled && scheduler.active.is_empty() { 519 | // Shutdown if there are no running actors (no progress can be made in such system). 520 | break 'main; 521 | } 522 | } 523 | } 524 | 525 | fn start_actor_runtime( 526 | name: String, 527 | host: String, 528 | pool: &ThreadPool, 529 | scheduler_config: SchedulerConfig, 530 | events: (Sender, Receiver), 531 | actions: (Sender, Receiver), 532 | ) { 533 | let (actions_tx, actions_rx) = actions; 534 | let (events_tx, events_rx) = events; 535 | 536 | let scheduler = Scheduler::with_config(&scheduler_config); 537 | 538 | let thread_count = scheduler.config.actor_worker_threads; 539 | for _ in 0..thread_count { 540 | let rx = events_rx.clone(); 541 | let tx = actions_tx.clone(); 542 | 543 | pool.submit(move || { 544 | worker_loop(tx, rx); 545 | }); 546 | } 547 | 548 | let background = pool.link(); 549 | pool.submit(move || { 550 | event_loop( 551 | actions_rx.clone(), 552 | actions_tx, 553 | events_tx.clone(), 554 | scheduler, 555 | background, 556 | name, 557 | host, 558 | ); 559 | for _ in 0..thread_count { 560 | events_tx.send(Event::Shutdown).unwrap(); 561 | } 562 | while actions_rx.recv().is_ok() { 563 | // Drain remaining actions sent from worker threads while they 564 | // (worker threads) are being shut down to avoid race condition 565 | // caused by actions_rx being dropped and worker threads that are 566 | // still running keep failing to send actions into closed channel. 567 | } 568 | }); 569 | } 570 | 571 | fn adjust_remote_address<'a>(address: &'a str, envelope: &'a mut Envelope) -> &'a str { 572 | if address.contains('@') { 573 | envelope.to = address.to_string(); 574 | return CLIENT; 575 | } 576 | address 577 | } 578 | 579 | fn report_logs( 580 | background: &impl Fn(Runnable), 581 | app: &str, 582 | host: &str, 583 | logs: Vec<(String, Vec<(SystemTime, String)>)>, 584 | ) { 585 | let app = app.to_string(); 586 | let host = host.to_string(); 587 | background(Box::new(move || { 588 | for (tag, stm) in logs { 589 | for (st, msg) in stm { 590 | let at = st 591 | .duration_since(SystemTime::UNIX_EPOCH) 592 | .unwrap() 593 | .as_millis() as u64; 594 | let log = LoggerEntry { 595 | at, 596 | meta: Meta { 597 | host: host.clone(), 598 | app: app.clone(), 599 | tag: tag.clone(), 600 | }, 601 | log: msg, 602 | }; 603 | println!("{:?}", log); 604 | } 605 | } 606 | })); 607 | } 608 | 609 | fn report_metrics( 610 | background: &impl Fn(Runnable), 611 | app: &str, 612 | host: &str, 613 | scheduler: SchedulerMetrics, 614 | metrics: HashMap>, 615 | ) { 616 | let app = app.to_string(); 617 | let host = host.to_string(); 618 | 619 | background(Box::new(move || { 620 | println!("{:?}", scheduler); 621 | metrics 622 | .into_iter() 623 | .flat_map(|(tag, entries)| { 624 | entries 625 | .into_iter() 626 | .map(|(st, val)| MetricEntry { 627 | at: st 628 | .duration_since(SystemTime::UNIX_EPOCH) 629 | .unwrap() 630 | .as_millis() as u64, 631 | meta: Meta { 632 | host: host.clone(), 633 | app: app.clone(), 634 | tag: tag.clone(), 635 | }, 636 | val, 637 | }) 638 | .collect::>() 639 | }) 640 | .for_each(|e| println!("{:?}", e)); 641 | })) 642 | } 643 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug)] 2 | pub enum Error { 3 | ThreadPoolTooSmall { required: usize, available: usize }, 4 | InvalidServerAddress(String), 5 | ServerBindFailed(u16), 6 | } 7 | 8 | impl From for Box { 9 | fn from(error: Error) -> Box { 10 | match error { 11 | Error::ThreadPoolTooSmall { 12 | required, 13 | available, 14 | } => format!( 15 | "Not enough threads in the pool: available={} but required={}", 16 | available, required 17 | ) 18 | .into(), 19 | Error::InvalidServerAddress(addr) => format!("Invalid server address: {}", addr).into(), 20 | Error::ServerBindFailed(port) => { 21 | format!("Failed to bind server on port {}", port).into() 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod api; 2 | pub mod config; 3 | pub mod core; 4 | pub mod error; 5 | pub mod monitor; 6 | pub mod pool; 7 | 8 | pub(crate) mod remote; 9 | -------------------------------------------------------------------------------- /src/monitor.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug)] 2 | pub struct Meta { 3 | pub host: String, 4 | pub app: String, 5 | pub tag: String, 6 | } 7 | 8 | // TODO Add JSON support 9 | #[derive(Debug)] 10 | pub struct LoggerEntry { 11 | pub at: u64, 12 | pub meta: Meta, 13 | pub log: String, 14 | } 15 | 16 | // TODO Add JSON support 17 | #[derive(Debug)] 18 | pub struct MetricEntry { 19 | pub at: u64, 20 | pub meta: Meta, 21 | pub val: f64, 22 | } 23 | 24 | #[derive(Default, Debug, Clone)] 25 | pub struct SchedulerMetrics { 26 | pub name: String, 27 | pub at: u64, 28 | pub ticks: u64, 29 | pub miss: u64, 30 | pub hit: u64, 31 | pub messages: u64, 32 | pub queues: u64, 33 | pub returns: u64, 34 | pub spawns: u64, 35 | pub delays: u64, 36 | pub stops: u64, 37 | pub drops: u64, 38 | pub failures: u64, 39 | pub actors: u64, 40 | } 41 | 42 | impl SchedulerMetrics { 43 | pub fn named(name: String) -> Self { 44 | Self { 45 | name, 46 | ..Default::default() 47 | } 48 | } 49 | 50 | pub fn reset(&mut self) { 51 | self.at = 0; 52 | self.ticks = 0; 53 | self.miss = 0; 54 | self.hit = 0; 55 | self.messages = 0; 56 | self.queues = 0; 57 | self.returns = 0; 58 | self.spawns = 0; 59 | self.delays = 0; 60 | self.stops = 0; 61 | self.drops = 0; 62 | self.failures = 0; 63 | self.actors = 0; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/pool.rs: -------------------------------------------------------------------------------- 1 | use crossbeam_channel::{unbounded, Receiver, Sender}; 2 | use std::panic::{self, AssertUnwindSafe}; 3 | use std::thread::{self, JoinHandle}; 4 | 5 | extern crate core_affinity; 6 | use crate::config::Config; 7 | use core_affinity::CoreId; 8 | 9 | pub type Runnable = Box; 10 | 11 | enum Job { 12 | Task(Runnable), 13 | Stop, 14 | } 15 | 16 | pub struct ThreadPool { 17 | sender: Sender, 18 | workers: Vec, 19 | } 20 | 21 | impl ThreadPool { 22 | pub fn for_config(config: &Config) -> ThreadPool { 23 | ThreadPool::new(config.scheduler.total_threads_required()) 24 | } 25 | 26 | pub fn new(size: usize) -> ThreadPool { 27 | let (sender, receiver) = unbounded(); 28 | let mut workers = Vec::with_capacity(size); 29 | 30 | let core_ids = core_affinity::get_core_ids().unwrap(); 31 | for idx in 0..size { 32 | let core_id = if idx < core_ids.len() { 33 | Some(core_ids.get(idx).unwrap().to_owned()) 34 | } else { 35 | None 36 | }; 37 | let worker = Worker::new(idx, receiver.clone(), core_id); 38 | workers.push(worker); 39 | } 40 | 41 | ThreadPool { sender, workers } 42 | } 43 | 44 | pub fn submit(&self, f: F) 45 | where 46 | F: FnOnce() + Send + 'static, 47 | { 48 | let job = Job::Task(Box::new(f)); 49 | self.sender.send(job).unwrap(); 50 | } 51 | 52 | pub fn size(&self) -> usize { 53 | self.workers.len() 54 | } 55 | 56 | pub fn link<'a>(&self) -> impl Fn(Runnable) + 'a { 57 | let sender = self.sender.clone(); 58 | move |r: Runnable| { 59 | sender.send(Job::Task(r)).unwrap(); 60 | } 61 | } 62 | } 63 | 64 | impl Drop for ThreadPool { 65 | fn drop(&mut self) { 66 | for _ in 0..self.workers.len() { 67 | self.sender.send(Job::Stop).unwrap(); 68 | } 69 | 70 | for worker in &mut self.workers { 71 | if let Some(thread) = worker.thread.take() { 72 | thread.join().unwrap(); 73 | } 74 | } 75 | } 76 | } 77 | 78 | struct Worker { 79 | thread: Option>, 80 | } 81 | 82 | impl Worker { 83 | fn new(id: usize, receiver: Receiver, core_id: Option) -> Worker { 84 | let thread = thread::Builder::new() 85 | .name(format!("worker-{}", id)) 86 | .spawn(move || { 87 | if let Some(id) = core_id { 88 | core_affinity::set_for_current(id); 89 | } 90 | loop { 91 | let job = receiver.recv(); 92 | match job { 93 | Ok(Job::Task(f)) => { 94 | let _ = panic::catch_unwind(AssertUnwindSafe(|| { 95 | f(); 96 | })); 97 | } 98 | Ok(Job::Stop) => break, 99 | Err(_) => break, 100 | } 101 | } 102 | }) 103 | .unwrap(); 104 | 105 | Worker { 106 | thread: Some(thread), 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/remote/client.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::error::Error; 3 | use std::io::{ErrorKind, Read, Write}; 4 | use std::net::SocketAddr; 5 | use std::time::Duration; 6 | 7 | use mio::net::TcpStream; 8 | use mio::{Events, Interest, Poll, Token}; 9 | 10 | use parsed::stream::ByteStream; 11 | 12 | use crate::api::{AnyActor, AnySender, Envelope}; 13 | use crate::config::ClientConfig; 14 | use crate::remote::packet::Packet; 15 | 16 | pub struct Client { 17 | config: ClientConfig, 18 | poll: Poll, 19 | events: Events, 20 | counter: usize, 21 | connections: HashMap, 22 | destinations: HashMap, 23 | response_port: u16, 24 | } 25 | 26 | impl Client { 27 | pub fn new(response_port: u16, config: &ClientConfig) -> Self { 28 | let poll = Poll::new().unwrap(); 29 | let events = Events::with_capacity(config.events_capacity); 30 | 31 | Client { 32 | config: config.clone(), 33 | poll, 34 | events, 35 | counter: 0, 36 | connections: HashMap::new(), 37 | destinations: HashMap::new(), 38 | response_port, 39 | } 40 | } 41 | 42 | pub fn connect(&mut self, target: &str) -> Result> { 43 | self.counter += 1; 44 | 45 | let addr: SocketAddr = target.parse()?; 46 | let mut socket = TcpStream::connect(addr)?; 47 | let id = self.counter; 48 | self.poll 49 | .registry() 50 | .register(&mut socket, Token(id), Interest::WRITABLE) 51 | .unwrap(); 52 | 53 | let connection = Connection::connected(addr.to_string(), socket, self.config.clone()); 54 | self.connections.insert(id, connection); 55 | self.destinations.insert(addr.to_string(), id); 56 | Ok(id) 57 | } 58 | 59 | pub fn poll(&mut self, timeout: Duration) { 60 | self.poll.poll(&mut self.events, Some(timeout)).unwrap(); 61 | 62 | for event in &self.events { 63 | let id = event.token().0; 64 | 65 | // TODO Consider extracting IO to worker/connection actors (server approach) 66 | let mut connection = self.connections.remove(&id).unwrap(); 67 | 68 | if event.is_readable() { 69 | let _ = connection.recv(); 70 | connection.is_open = !event.is_read_closed(); 71 | } 72 | 73 | if event.is_writable() { 74 | connection.send(); 75 | connection.send_buf.pull(); 76 | connection.is_open = !event.is_write_closed(); 77 | } 78 | 79 | if connection.is_open { 80 | self.poll 81 | .registry() 82 | .reregister( 83 | connection.socket.as_mut().unwrap(), 84 | event.token(), 85 | Interest::READABLE.add(Interest::WRITABLE), 86 | ) 87 | .unwrap(); 88 | 89 | self.connections.insert(id, connection); 90 | } else { 91 | self.destinations.remove(&connection.target); 92 | } 93 | } 94 | } 95 | 96 | pub fn has(&self, addr: &str) -> bool { 97 | self.destinations.contains_key(addr) 98 | } 99 | 100 | pub fn put(&mut self, addr: &str, payload: &[u8]) -> usize { 101 | let id = self.destinations.get(addr).unwrap(); 102 | self.connections.get_mut(id).unwrap().send_buf.put(payload) 103 | } 104 | } 105 | 106 | struct Connection { 107 | target: String, 108 | socket: Option, 109 | is_open: bool, 110 | recv_buf: ByteStream, 111 | send_buf: ByteStream, 112 | } 113 | 114 | impl Connection { 115 | fn connected(target: String, socket: TcpStream, config: ClientConfig) -> Self { 116 | Self { 117 | target, 118 | socket: Some(socket), 119 | is_open: true, 120 | recv_buf: ByteStream::with_capacity(config.recv_buffer_size), 121 | send_buf: ByteStream::with_capacity(config.send_buffer_size), 122 | } 123 | } 124 | } 125 | 126 | impl Connection { 127 | fn send(&mut self) { 128 | match self 129 | .socket 130 | .as_ref() 131 | .unwrap() 132 | .write_all(self.send_buf.as_ref()) 133 | { 134 | Ok(_) => { 135 | self.send_buf.clear(); 136 | } 137 | Err(_) => { 138 | self.is_open = false; 139 | } 140 | } 141 | } 142 | 143 | fn recv(&mut self) -> usize { 144 | let mut bytes_received: usize = 0; 145 | let mut buffer = [0u8; 1024]; 146 | while self.recv_buf.cap() >= buffer.len() { 147 | match self.socket.as_ref().unwrap().read(&mut buffer) { 148 | Ok(n) if n > 0 => { 149 | self.recv_buf.put(&buffer[0..n]); 150 | bytes_received += n; 151 | } 152 | Err(e) if e.kind() == ErrorKind::WouldBlock => break, 153 | Ok(_) | Err(_) => { 154 | self.is_open = false; 155 | break; 156 | } 157 | } 158 | } 159 | bytes_received 160 | } 161 | } 162 | 163 | #[derive(Debug)] 164 | pub(crate) struct Loop; 165 | 166 | impl AnyActor for Client { 167 | fn receive(&mut self, envelope: Envelope, sender: &mut dyn AnySender) { 168 | if envelope.message.downcast_ref::().is_some() { 169 | self.poll(Duration::from_millis(1)); 170 | sender.send(sender.me(), envelope); 171 | } else if let Some(payload) = envelope.message.downcast_ref::>() { 172 | let (to, host) = split_address(envelope.to); 173 | let from = envelope.from.to_owned(); 174 | 175 | let ok = self.has(&host) || self.connect(&host).is_ok(); 176 | if ok { 177 | let packet = Packet::new(to, from, payload.to_vec(), self.response_port); 178 | self.put(&host, packet.to_bytes().as_ref()); 179 | sender.log(&format!( 180 | "sent: to={}[@{}] from={} bytes={}", 181 | packet.to, 182 | host, 183 | packet.from, 184 | payload.len() 185 | )); 186 | } else { 187 | sender.log(&format!( 188 | "Failed to send: to={}[@{}] from={} bytes={}", 189 | to, 190 | host, 191 | from, 192 | payload.len() 193 | )); 194 | } 195 | } 196 | } 197 | } 198 | 199 | fn split_address(address: String) -> (String, String) { 200 | if address.contains('@') { 201 | let split = address 202 | .split('@') 203 | .map(|s| s.to_string()) 204 | .collect::>(); 205 | (split[0].to_owned(), split[1].to_owned()) 206 | } else { 207 | (address, String::default()) 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/remote/ip.rs: -------------------------------------------------------------------------------- 1 | extern crate get_if_addrs; 2 | use std::net::IpAddr; 3 | 4 | #[allow(dead_code)] // unused for now, but might be useful in future 5 | pub fn host() -> IpAddr { 6 | get_if_addrs::get_if_addrs() 7 | .unwrap_or_default() 8 | .into_iter() 9 | .find(|i| !i.is_loopback()) 10 | .map(|i| i.addr.ip()) 11 | .unwrap_or_else(|| IpAddr::from([127, 0, 0, 1])) 12 | } 13 | -------------------------------------------------------------------------------- /src/remote/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod client; 2 | pub mod packet; 3 | pub mod server; 4 | 5 | #[cfg(feature = "host")] 6 | pub mod ip; 7 | -------------------------------------------------------------------------------- /src/remote/packet.rs: -------------------------------------------------------------------------------- 1 | use bytes::{BufMut, Bytes, BytesMut}; 2 | 3 | use parsed::stream::ByteStream; 4 | use std::mem::size_of; 5 | 6 | pub struct Packet { 7 | pub to: String, 8 | pub from: String, 9 | pub payload: Vec, 10 | pub port: u16, 11 | } 12 | 13 | impl Packet { 14 | pub fn new(to: String, from: String, payload: Vec, port: u16) -> Self { 15 | Self { 16 | to, 17 | from, 18 | payload, 19 | port, 20 | } 21 | } 22 | 23 | pub fn to_bytes(&self) -> Bytes { 24 | let cap: usize = 3 * size_of::() 25 | + size_of::() 26 | + self.payload.len() 27 | + self.to.len() 28 | + self.from.len(); 29 | let mut bytes = BytesMut::with_capacity(cap); 30 | bytes.put_u32(self.to.len() as u32); 31 | bytes.put_u32(self.from.len() as u32); 32 | bytes.put_u32(self.payload.len() as u32); 33 | bytes.put_u16(self.port); 34 | bytes.put(self.to.as_bytes()); 35 | bytes.put(self.from.as_bytes()); 36 | bytes.put(self.payload.as_ref()); 37 | bytes.freeze() 38 | } 39 | 40 | pub fn from_bytes(stream: &mut ByteStream, limit: usize) -> Result, ()> { 41 | let min = 3 * size_of::() + size_of::(); 42 | if stream.len() <= min { 43 | return Ok(None); 44 | } 45 | let (to_len, from_len, payload_len, port) = ( 46 | stream.get_u32().unwrap() as usize, 47 | stream.get_u32().unwrap() as usize, 48 | stream.get_u32().unwrap() as usize, 49 | stream.get_u16().unwrap(), 50 | ); 51 | let total = min + to_len + from_len + payload_len; 52 | if total > limit { 53 | // Naive check if such stream will ever match into a Packet 54 | return Err(()); 55 | } 56 | if stream.len() < total { 57 | return Ok(None); 58 | } 59 | 60 | let to = String::from_utf8(stream.get(to_len).unwrap()).unwrap(); 61 | let from = String::from_utf8(stream.get(from_len).unwrap()).unwrap(); 62 | let payload = stream.get(payload_len).unwrap(); 63 | stream.pull(); 64 | 65 | Ok(Some(Packet { 66 | to, 67 | from, 68 | payload, 69 | port, 70 | })) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/remote/server.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Write}; 2 | use std::net::SocketAddr; 3 | use std::time::Duration; 4 | 5 | use mio::net::{TcpListener, TcpStream}; 6 | use mio::{Events, Interest, Poll, Token}; 7 | 8 | use parsed::stream::ByteStream; 9 | 10 | use crate::api::{AnyActor, AnySender, Envelope}; 11 | use crate::config::ServerConfig; 12 | use crate::error::Error; 13 | use crate::remote::packet::Packet; 14 | 15 | #[derive(Debug)] 16 | pub(crate) struct Loop; 17 | 18 | #[derive(Debug)] 19 | struct Work { 20 | is_readable: bool, 21 | is_writable: bool, 22 | } 23 | 24 | pub struct Server { 25 | config: ServerConfig, 26 | poll: Poll, 27 | events: Events, 28 | socket: TcpListener, 29 | counter: usize, 30 | port: u16, 31 | } 32 | 33 | // TODO Make configurable? 34 | const PACKET_SIZE_LIMIT: usize = 4096 + 12; // 12 bytes header + max 4kb payload 35 | 36 | impl Server { 37 | pub fn listen(addr: &str, config: &ServerConfig) -> Result { 38 | let poll = Poll::new().unwrap(); 39 | let events = Events::with_capacity(config.events_capacity); 40 | let addr = addr 41 | .parse::() 42 | .map_err(|_| Error::InvalidServerAddress(addr.to_string()))?; 43 | let port = addr.port(); 44 | let mut socket = TcpListener::bind(addr).map_err(|_| Error::ServerBindFailed(port))?; 45 | poll.registry() 46 | .register(&mut socket, Token(0), Interest::READABLE) 47 | .unwrap(); 48 | 49 | let listener = Server { 50 | config: config.clone(), 51 | poll, 52 | events, 53 | socket, 54 | counter: 0, 55 | port, 56 | }; 57 | Ok(listener) 58 | } 59 | 60 | pub fn port(&self) -> u16 { 61 | self.port 62 | } 63 | } 64 | 65 | impl Server { 66 | fn tag(me: &str, id: usize) -> String { 67 | format!("{}#{:012}", me, id) 68 | } 69 | } 70 | 71 | impl AnyActor for Server { 72 | fn receive(&mut self, envelope: Envelope, sender: &mut dyn AnySender) { 73 | if envelope.message.downcast_ref::().is_some() { 74 | self.poll 75 | .poll(&mut self.events, Some(Duration::from_millis(1))) 76 | .unwrap(); 77 | for event in self.events.iter() { 78 | match event.token() { 79 | Token(0) => { 80 | while let Ok((mut socket, _remote)) = self.socket.accept() { 81 | self.counter += 1; 82 | let token = Token(self.counter); 83 | self.poll 84 | .registry() 85 | .register( 86 | &mut socket, 87 | token, 88 | Interest::READABLE | Interest::WRITABLE, 89 | ) 90 | .unwrap(); 91 | let connection = Connection { 92 | socket, 93 | keep_alive: true, 94 | recv_buf: ByteStream::with_capacity(self.config.recv_buffer_size), 95 | send_buf: ByteStream::with_capacity(self.config.send_buffer_size), 96 | is_open: true, 97 | can_read: false, 98 | can_write: false, 99 | }; 100 | let tag = Server::tag(sender.me(), self.counter); 101 | sender.spawn(&tag, Box::new(move || Box::new(connection))); 102 | } 103 | } 104 | token => { 105 | let tag = Server::tag(sender.me(), token.0); 106 | let work = Work { 107 | is_readable: event.is_readable(), 108 | is_writable: event.is_writable(), 109 | }; 110 | sender.send(&tag, Envelope::of(work)); 111 | } 112 | } 113 | } 114 | sender.send(sender.me(), Envelope::of(Loop)); 115 | } 116 | } 117 | } 118 | 119 | struct Connection { 120 | socket: TcpStream, 121 | is_open: bool, 122 | keep_alive: bool, 123 | recv_buf: ByteStream, 124 | send_buf: ByteStream, 125 | can_read: bool, 126 | can_write: bool, 127 | } 128 | 129 | impl Connection { 130 | fn keep_open(&mut self, sender: &mut dyn AnySender) -> bool { 131 | if !self.is_open { 132 | if !self.keep_alive { 133 | self.is_open = true; 134 | } else { 135 | sender.stop(sender.me()); 136 | } 137 | } 138 | self.is_open 139 | } 140 | } 141 | 142 | impl AnyActor for Connection { 143 | fn receive(&mut self, envelope: Envelope, sender: &mut dyn AnySender) { 144 | if let Some(work) = envelope.message.downcast_ref::() { 145 | let mut buffer = [0u8; 1024]; 146 | self.can_read = work.is_readable; 147 | self.can_write = self.can_write || work.is_writable; 148 | if self.can_read { 149 | match self.socket.read(&mut buffer[..]) { 150 | Ok(0) | Err(_) => { 151 | self.is_open = false; 152 | } 153 | Ok(n) => { 154 | self.recv_buf.put(&buffer[0..n]); 155 | } 156 | } 157 | } 158 | 159 | if !self.keep_open(sender) { 160 | return; 161 | } 162 | 163 | loop { 164 | let r = Packet::from_bytes(&mut self.recv_buf, PACKET_SIZE_LIMIT); 165 | if r.is_err() { 166 | sender.log("Packet parser marked connection buffer as failed"); 167 | self.is_open = false; 168 | break; 169 | } 170 | 171 | if let Ok(Some(packet)) = r { 172 | let mut host = self.socket.peer_addr().unwrap(); 173 | host.set_port(packet.port); 174 | let from = format!("{}@{}", packet.from, host); 175 | 176 | sender.log(&format!( 177 | "server/rcvd: to={} from={} bytes={}", 178 | packet.to, 179 | packet.from, 180 | packet.payload.len() 181 | )); 182 | let e = Envelope::of(packet.payload).to(&packet.to).from(&from); 183 | sender.send(&packet.to, e); 184 | } else { 185 | break; 186 | } 187 | } 188 | 189 | if self.can_write && self.send_buf.len() > 0 { 190 | match self.socket.write_all(self.send_buf.as_ref()) { 191 | Ok(_) => { 192 | self.send_buf.clear(); 193 | } 194 | _ => { 195 | self.is_open = false; 196 | } 197 | } 198 | } 199 | 200 | self.keep_open(sender); 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /tests/actors.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | use std::ops::Add; 3 | use std::sync::mpsc::{channel, RecvTimeoutError, Sender}; 4 | use std::time::Duration; 5 | 6 | use uppercut::api::{AnyActor, AnySender, Envelope}; 7 | use uppercut::config::Config; 8 | use uppercut::core::{Run, System}; 9 | use uppercut::pool::ThreadPool; 10 | 11 | const ANSWER: usize = 42; 12 | 13 | #[derive(Debug)] 14 | struct Init(Sender); 15 | 16 | struct Test(usize); 17 | 18 | impl AnyActor for Test { 19 | fn receive(&mut self, envelope: Envelope, _sender: &mut dyn AnySender) { 20 | if let Some(message) = envelope.message.downcast_ref::() { 21 | message.0.send(self.0).unwrap(); 22 | } 23 | } 24 | } 25 | 26 | struct Proxy { 27 | target: String, 28 | } 29 | 30 | impl AnyActor for Proxy { 31 | fn receive(&mut self, envelope: Envelope, sender: &mut dyn AnySender) { 32 | sender.send(&self.target, envelope); 33 | } 34 | } 35 | 36 | #[derive(Debug)] 37 | struct Counter(usize, Sender); 38 | 39 | #[derive(Debug)] 40 | enum CounterProtocol { 41 | Inc, 42 | Get, 43 | } 44 | 45 | impl AnyActor for Counter { 46 | fn receive(&mut self, envelope: Envelope, sender: &mut dyn AnySender) { 47 | if let Some(p) = envelope.message.downcast_ref::() { 48 | match p { 49 | CounterProtocol::Inc => { 50 | self.0 += 1; 51 | self.1.send(self.0).unwrap(); 52 | } 53 | CounterProtocol::Get => { 54 | let env = Envelope::of(CounterProtocol::Inc); 55 | sender.send(sender.me(), env); 56 | } 57 | } 58 | } 59 | } 60 | } 61 | 62 | fn with_run Result>(expected: T, f: F) -> Result<(), E> { 63 | let cfg = Config::default(); 64 | let pool = ThreadPool::for_config(&cfg); 65 | let sys = System::new("test", "localhost", &cfg); 66 | let run = sys.run(&pool).unwrap(); 67 | let got = f(&run); 68 | run.shutdown(); 69 | let actual = got?; 70 | assert_eq!(actual, expected); 71 | Ok(()) 72 | } 73 | 74 | const TIMEOUT: Duration = Duration::from_millis(500); 75 | 76 | #[test] 77 | fn sent_message_received() -> Result<(), RecvTimeoutError> { 78 | with_run(ANSWER, |run| { 79 | run.spawn("test", || Box::new(Test(ANSWER))); 80 | 81 | let (tx, rx) = channel(); 82 | let env = Envelope::of(Init(tx)); 83 | run.send("test", env); 84 | 85 | rx.recv_timeout(TIMEOUT) 86 | }) 87 | } 88 | 89 | #[test] 90 | fn forwarded_message_received() -> Result<(), RecvTimeoutError> { 91 | with_run(ANSWER, |run| { 92 | run.spawn("test", || Box::new(Test(ANSWER))); 93 | run.spawn("proxy", || { 94 | Box::new(Proxy { 95 | target: "test".to_string(), 96 | }) 97 | }); 98 | 99 | let (tx, rx) = channel(); 100 | let env = Envelope::of(Init(tx)); 101 | run.send("proxy", env); 102 | 103 | rx.recv_timeout(TIMEOUT) 104 | }) 105 | } 106 | 107 | #[test] 108 | fn delayed_message_received() -> Result<(), RecvTimeoutError> { 109 | with_run(ANSWER, |run| { 110 | run.spawn("test", || Box::new(Test(ANSWER))); 111 | 112 | let (tx, rx) = channel(); 113 | let env = Envelope::of(Init(tx)); 114 | 115 | const DELAY: Duration = Duration::from_millis(100); 116 | run.delay("test", env, DELAY); 117 | 118 | rx.recv_timeout(TIMEOUT.add(DELAY)) 119 | }) 120 | } 121 | 122 | #[test] 123 | fn own_message_received() -> Result<(), RecvTimeoutError> { 124 | with_run(ANSWER + 1, |run| { 125 | let (tx, rx) = channel(); 126 | run.spawn("test", || Box::new(Counter(ANSWER, tx))); 127 | 128 | let env = Envelope::of(CounterProtocol::Get); 129 | run.send("test", env); 130 | 131 | rx.recv_timeout(TIMEOUT) 132 | }) 133 | } 134 | 135 | struct Replier(Sender); 136 | 137 | impl AnyActor for Replier { 138 | fn receive(&mut self, envelope: Envelope, _sender: &mut dyn AnySender) { 139 | if let Some(n) = envelope.message.downcast_ref::() { 140 | self.0.send(*n).unwrap(); 141 | } 142 | } 143 | } 144 | 145 | #[test] 146 | fn message_order_perceived() -> Result<(), RecvTimeoutError> { 147 | let n = 10; 148 | let seq: Vec = (1..=n).into_iter().collect(); 149 | with_run(seq, |run| { 150 | let (tx, rx) = channel(); 151 | run.spawn("test", || Box::new(Replier(tx))); 152 | 153 | for x in 1..=n { 154 | let e = Envelope::of(x); 155 | run.send("test", e); 156 | } 157 | 158 | let mut vec: Vec = Vec::with_capacity(n); 159 | for _ in 0..n { 160 | let x = rx.recv_timeout(TIMEOUT)?; 161 | vec.push(x); 162 | } 163 | Ok(vec) 164 | }) 165 | } 166 | 167 | struct Fan(String, usize); 168 | 169 | impl AnyActor for Fan { 170 | fn receive(&mut self, envelope: Envelope, sender: &mut dyn AnySender) { 171 | if let Some(n) = envelope.message.downcast_ref::() { 172 | for x in 0..*n { 173 | let response = Envelope::of((x, self.1)); 174 | sender.delay(&self.0, response, Duration::from_millis(100)); 175 | self.1 += 1; 176 | } 177 | } 178 | } 179 | } 180 | 181 | struct Echo(Sender<(usize, usize)>); 182 | 183 | impl AnyActor for Echo { 184 | fn receive(&mut self, envelope: Envelope, _sender: &mut dyn AnySender) { 185 | if let Some(pair) = envelope.message.downcast_ref::<(usize, usize)>() { 186 | self.0.send(pair.to_owned()).unwrap(); 187 | } 188 | } 189 | } 190 | 191 | #[test] 192 | fn delayed_messages_ordering() -> Result<(), RecvTimeoutError> { 193 | const N: usize = 3; 194 | let seq: Vec = (0..N).into_iter().collect(); 195 | let expected: Vec<(usize, usize)> = seq.iter().map(|x| (*x, *x)).collect(); 196 | with_run(expected, |run| { 197 | let (tx, rx) = channel(); 198 | run.spawn("echo", || Box::new(Echo(tx))); 199 | run.spawn("seq", || Box::new(Fan("echo".to_string(), 0))); 200 | 201 | run.send("seq", Envelope::of(N)); 202 | let mut actual = Vec::with_capacity(N); 203 | for _ in 0..N { 204 | let x = rx.recv_timeout(TIMEOUT)?; 205 | actual.push(x); 206 | } 207 | 208 | Ok(actual) 209 | }) 210 | } 211 | -------------------------------------------------------------------------------- /tests/remote.rs: -------------------------------------------------------------------------------- 1 | mod ping_test { 2 | use std::fmt::Debug; 3 | use std::sync::mpsc::Sender; 4 | 5 | use uppercut::api::{AnyActor, AnySender, Envelope}; 6 | 7 | #[derive(Default)] 8 | pub struct Tester { 9 | pub tx: Option>>, 10 | } 11 | 12 | #[derive(Debug)] 13 | pub struct Probe(pub Sender>); 14 | 15 | #[derive(Debug)] 16 | pub struct Forward(pub Vec, pub String); 17 | 18 | impl AnyActor for Tester { 19 | fn receive(&mut self, envelope: Envelope, sender: &mut dyn AnySender) { 20 | if let Some(Forward(payload, target)) = envelope.message.downcast_ref::() { 21 | sender.log(&format!("Forward '{:?}' to '{}' received", payload, target)); 22 | let envelope = Envelope::of(payload.to_vec()); 23 | sender.send(target, envelope); 24 | } else if let Some(Probe(tx)) = envelope.message.downcast_ref::() { 25 | sender.log("probe accepted"); 26 | self.tx = Some(tx.clone()); 27 | } else if let Some(vec) = envelope.message.downcast_ref::>() { 28 | sender.log("payload received"); 29 | self.tx.as_ref().unwrap().send(vec.clone()).unwrap(); 30 | } 31 | } 32 | } 33 | } 34 | 35 | #[test] 36 | fn test_remote_ping_pong() { 37 | use crate::ping_test::{Forward, Probe, Tester}; 38 | use std::sync::mpsc::channel; 39 | use std::time::Duration; 40 | use uppercut::api::Envelope; 41 | use uppercut::config::{ClientConfig, Config, RemoteConfig, SchedulerConfig, ServerConfig}; 42 | use uppercut::core::System; 43 | use uppercut::pool::ThreadPool; 44 | 45 | const TIMEOUT: Duration = Duration::from_millis(300); 46 | 47 | let cores = 4; 48 | let pool = ThreadPool::new(cores + 4); 49 | 50 | let cfg1 = Config::new( 51 | SchedulerConfig::with_total_threads(cores / 2), 52 | RemoteConfig::listening_at( 53 | "127.0.0.1:10001", 54 | ServerConfig::default(), 55 | ClientConfig::default(), 56 | ), 57 | ); 58 | let sys1 = System::new("A", "localhost", &cfg1); 59 | let run1 = sys1.run(&pool).unwrap(); 60 | 61 | let cfg2 = Config::new( 62 | SchedulerConfig::with_total_threads(cores / 2), 63 | RemoteConfig::listening_at( 64 | "127.0.0.1:10002", 65 | ServerConfig::default(), 66 | ClientConfig::default(), 67 | ), 68 | ); 69 | let sys2 = System::new("B", "localhost", &cfg2); 70 | let run2 = sys2.run(&pool).unwrap(); 71 | 72 | let address = "address"; 73 | let payload = b"hello!".to_vec(); 74 | run1.spawn_default::(address); 75 | run2.spawn_default::(address); 76 | 77 | let (tx, rx) = channel(); 78 | run2.send(address, Envelope::of(Probe(tx))); 79 | run1.send( 80 | address, 81 | Envelope::of(Forward( 82 | payload.clone(), 83 | "address@127.0.0.1:10002".to_string(), 84 | )), 85 | ); 86 | 87 | let result = rx.recv_timeout(TIMEOUT); 88 | run1.shutdown(); 89 | run2.shutdown(); 90 | if let Ok(received) = result { 91 | assert_eq!(received, payload); 92 | } else { 93 | panic!("Probe did not receive forwarded payload"); 94 | } 95 | } 96 | 97 | mod reply_test { 98 | use std::fmt::Debug; 99 | use std::sync::mpsc::Sender; 100 | use uppercut::api::{AnyActor, AnySender, Envelope}; 101 | 102 | #[derive(Default)] 103 | pub struct Spy { 104 | pub tx: Option>, 105 | } 106 | 107 | #[derive(Debug)] 108 | pub struct Probe { 109 | pub tx: Sender, 110 | pub message: String, 111 | pub target: String, 112 | } 113 | 114 | impl AnyActor for Spy { 115 | fn receive(&mut self, envelope: Envelope, sender: &mut dyn AnySender) { 116 | if let Some(probe) = envelope.message.downcast_ref::() { 117 | sender.log("probe received"); 118 | self.tx = Some(probe.tx.clone()); 119 | let env = Envelope::of(probe.message.as_bytes().to_vec()) 120 | .to(&probe.target) 121 | .from(sender.me()); 122 | sender.log(&format!( 123 | "sent message '{}' to '{}'", 124 | probe.message, probe.target 125 | )); 126 | sender.send(&probe.target, env); 127 | } else if let Some(buf) = envelope.message.downcast_ref::>() { 128 | sender.log("response received"); 129 | let message = 130 | String::from_utf8(buf.clone()).unwrap_or_else(|_| "".to_string()); 131 | self.tx.as_ref().unwrap().send(message).unwrap(); 132 | } 133 | } 134 | } 135 | 136 | #[derive(Default)] 137 | pub struct Echo; 138 | 139 | impl AnyActor for Echo { 140 | fn receive(&mut self, envelope: Envelope, sender: &mut dyn AnySender) { 141 | if let Some(buf) = envelope.message.downcast_ref::>() { 142 | sender.log(&format!("received envelope from {}", envelope.from)); 143 | let reply = Envelope::of(buf.to_vec()) 144 | .to(&envelope.from) 145 | .from(&envelope.to); 146 | sender.log(&format!("sending echo to '{}'", reply.to)); 147 | sender.send(&envelope.from, reply); 148 | } 149 | } 150 | } 151 | } 152 | 153 | #[test] 154 | fn test_remote_reply() { 155 | use crate::reply_test::*; 156 | use std::sync::mpsc::channel; 157 | use std::time::Duration; 158 | use uppercut::api::Envelope; 159 | use uppercut::config::{ClientConfig, Config, RemoteConfig, SchedulerConfig, ServerConfig}; 160 | use uppercut::core::System; 161 | use uppercut::pool::ThreadPool; 162 | 163 | const TIMEOUT: Duration = Duration::from_millis(10000); 164 | 165 | let cores = 4; 166 | let pool = ThreadPool::new(cores + 4); 167 | 168 | let cfg1 = Config::new( 169 | SchedulerConfig::with_total_threads(cores / 2), 170 | RemoteConfig::listening_at( 171 | "127.0.0.1:20001", 172 | ServerConfig::default(), 173 | ClientConfig::default(), 174 | ), 175 | ); 176 | let sys1 = System::new("A", "localhost", &cfg1); 177 | let run1 = sys1.run(&pool).unwrap(); 178 | 179 | let cfg2 = Config::new( 180 | SchedulerConfig::with_total_threads(cores / 2), 181 | RemoteConfig::listening_at( 182 | "127.0.0.1:20002", 183 | ServerConfig::default(), 184 | ClientConfig::default(), 185 | ), 186 | ); 187 | let sys2 = System::new("B", "localhost", &cfg2); 188 | let run2 = sys2.run(&pool).unwrap(); 189 | 190 | run1.spawn_default::("spy"); 191 | run2.spawn_default::("echo"); 192 | 193 | let message = "Answer is 42.".to_string(); 194 | let (tx, rx) = channel(); 195 | let env1 = Envelope::of(Probe { 196 | tx, 197 | message: message.clone(), 198 | target: "echo@127.0.0.1:20002".to_string(), 199 | }); 200 | run1.send("spy", env1); 201 | 202 | let result = rx.recv_timeout(TIMEOUT); 203 | run1.shutdown(); 204 | run2.shutdown(); 205 | if let Ok(received) = result { 206 | assert_eq!(received, message); 207 | } else { 208 | panic!("Probe did not receive forwarded payload"); 209 | } 210 | } 211 | --------------------------------------------------------------------------------