├── .dockerignore ├── .editorconfig ├── .github ├── BUMP └── workflows │ ├── go-helpers.yml │ ├── go-history.yml │ ├── go-machine.yml │ ├── go-node.yml │ ├── go-pubsub.yml │ ├── go-rpc.yml │ ├── go-states.yml │ ├── go-telemetry.yml │ ├── go-tools.yml │ ├── go.yml │ ├── linter.yml │ └── releaser.yml ├── .github_changelog_generator ├── .gitignore ├── .gitmodules ├── .golangci.yml ├── .goreleaser.yml ├── BREAKING.md ├── CHANGELOG.md ├── FAQ.md ├── LICENSE ├── README.md ├── ROADMAP.md ├── Taskfile.yml ├── codecov.yaml ├── config ├── .mdl_style.rb ├── .mdlrc ├── cli │ └── debug-am-dbg.txt ├── dashboards │ ├── dash-1x1.kdl │ ├── dash-2x1.kdl │ └── dash-2x1x1.kdl ├── env │ ├── debug-node.env │ ├── debug-pubsub.env │ ├── debug-rpc.env │ ├── debug-simple.env │ ├── debug-telemetry-dbg.env │ ├── debug-tests.env │ └── tests-remote.env └── terminalizer.yml ├── deploy └── web-metrics │ ├── docker-compose.yml │ ├── loki.yaml │ ├── otel-collector-config.yaml │ └── prometheus.yaml ├── docs ├── cookbook.md ├── diagrams-examples.md ├── diagrams.md ├── env-configs.md ├── jsonschema │ ├── getter_req.json │ ├── getter_resp.json │ ├── msg_kind_req.json │ ├── msg_kind_resp.json │ ├── mutation_req.json │ ├── mutation_resp.json │ ├── waiting_req.json │ └── waiting_resp.json └── manual.md ├── examples ├── .editorconfig ├── README.md ├── arpc │ ├── Procfile │ ├── Taskfile.yml │ ├── client │ │ └── main.go │ ├── server │ │ └── main.go │ └── states │ │ ├── ss_example.go │ │ └── states_utils.go ├── asynq_fileprocessing │ ├── client │ │ └── client.go │ ├── fileprocessing_task.go │ └── worker │ │ └── worker.go ├── benchmark_grpc │ ├── README.md │ ├── client_arpc_test.go │ ├── client_grpc_test.go │ ├── client_local_test.go │ ├── proto │ │ ├── worker.pb.go │ │ ├── worker.proto │ │ └── worker_grpc.pb.go │ ├── server_arpc.go │ ├── server_grpc.go │ ├── states │ │ ├── ss_worker.go │ │ └── states_utils.go │ ├── worker.go │ ├── worker_proto │ │ ├── worker.pb.go │ │ ├── worker.proto │ │ └── worker_grpc.pb.go │ └── worker_states │ │ └── worker_states.go ├── benchmark_libp2p_pubsub │ └── README.md ├── benchmark_state_source │ ├── Dockerfile │ ├── README.md │ ├── Taskfile.yml │ ├── bench │ │ ├── .gitignore │ │ ├── Caddyfile-root │ │ ├── Caddyfile-root-2 │ │ ├── Caddyfile-root-2-4 │ │ ├── Caddyfile-root-2-6 │ │ ├── Dockerfile │ │ └── go-wrk.sh │ └── docker-compose.yml ├── dag_dependency_graph │ └── dependency_graph.go ├── fan_out_in │ └── example_fan_out_in.go ├── fsm │ └── fsm_test.go ├── mach_template │ ├── example.env │ ├── gen-states.sh │ ├── mach_template.go │ └── states │ │ ├── ss_mach_template.go │ │ └── states_utils.go ├── nfa │ ├── nfa_test.go │ └── states │ │ ├── ss_nfa.go │ │ └── states_utils.go ├── path_watcher │ ├── states │ │ └── ss_watcher.go │ └── watcher.go ├── pipes │ └── example_pipes.go ├── raw_strings │ └── raw_strings.go ├── relations_playground │ ├── relations_playground.go │ └── relations_playground_test.go ├── repl │ ├── main.go │ └── states │ │ ├── ss_example.go │ │ └── states_utils.go ├── subscriptions │ └── example_subscriptions.go ├── temporal_expense │ ├── expense_test.go │ └── states │ │ └── ss_expense.go ├── temporal_fileprocessing │ ├── fileprocessing.go │ ├── fileprocessing_test.go │ └── states │ │ └── ss_fileprocessing.go └── tree_state_source │ ├── Procfile │ ├── README.md │ ├── Taskfile.yml │ ├── cmd_state_node.go │ ├── config.env │ ├── gen-grafana.sh │ ├── gen_states │ └── gen_states.go │ └── states │ ├── ss_flights.go │ └── states_utils.go ├── go.mod ├── go.sum ├── internal ├── cmd │ └── am-dbg-video │ │ └── cmd_dbg_video.go ├── testing │ ├── cmd │ │ └── am-dbg-worker │ │ │ └── main_dbg_worker.go │ ├── states │ │ └── ss_rel.go │ ├── testing.go │ └── utils │ │ └── test_utils.go └── utils │ └── utils.go ├── pkg ├── graph │ └── graph.go ├── helpers │ ├── README.md │ ├── help.go │ ├── help_test.go │ └── testing │ │ └── test_help.go ├── history │ ├── README.md │ ├── history.go │ └── history_test.go ├── integrations │ ├── README.md │ ├── integrations.go │ └── nats │ │ ├── nats.go │ │ └── nats_test.go ├── machine │ ├── README.md │ ├── machine.go │ ├── machine_test.go │ ├── misc_mach.go │ ├── misc_mach_test.go │ ├── resolver.go │ ├── transition.go │ ├── types.go │ ├── typesafe.go │ ├── utils.go │ └── utils_test.go ├── node │ ├── README.md │ ├── node.go │ ├── node_client.go │ ├── node_test.go │ ├── node_worker.go │ ├── states │ │ ├── ss_bootstrap.go │ │ ├── ss_node_client.go │ │ ├── ss_node_worker.go │ │ └── ss_supervisor.go │ ├── supervisor.go │ ├── supervisor_misc.go │ └── test │ │ └── worker │ │ └── node_test_worker.go ├── pubsub │ ├── README.md │ ├── pubsub.go │ ├── pubsub_test.go │ ├── states │ │ └── ss_topic.go │ └── topic.go ├── rpc │ ├── HOWTO.md │ ├── README.md │ ├── mux.go │ ├── rpc.go │ ├── rpc_client.go │ ├── rpc_machine_test.go │ ├── rpc_server.go │ ├── rpc_test.go │ ├── rpc_worker.go │ ├── rpcnames │ │ └── rpcnames.go │ ├── states │ │ ├── ss_mux.go │ │ ├── ss_rpc_client.go │ │ ├── ss_rpc_consumer.go │ │ ├── ss_rpc_server.go │ │ ├── ss_rpc_shared.go │ │ └── ss_rpc_worker.go │ └── utils_test.go ├── states │ ├── README.md │ ├── global │ │ └── states_utils.go │ ├── pipes │ │ └── pipes.go │ ├── ss_basic.go │ ├── ss_connected.go │ ├── ss_disposed.go │ └── states_utils.go ├── telemetry │ ├── README.md │ ├── dbg.go │ ├── otel.go │ ├── otel_test.go │ ├── prometheus │ │ └── prometheus.go │ └── telemetry.go └── x │ └── helpers │ ├── x_helpers.go │ └── x_helpers_test.go ├── scripts ├── compact_number │ └── compact_number.go ├── dep-taskfile.sh ├── extract_mermaid │ └── extract_mermaid.go ├── gen_jsonschema │ └── gen_jsonschema.go ├── test-loop-record.sh ├── test-loop-replay.sh └── test-loop.sh └── tools ├── cmd ├── am-dbg-ssh │ └── cmd_dbg_ssh.go ├── am-dbg │ ├── README.md │ └── cmd_dbg.go ├── am-gen │ ├── README.md │ └── cmd_gen.go └── arpc │ ├── README.md │ └── cmd_arpc.go ├── debugger ├── README.md ├── cli │ └── cli_dbg.go ├── client_list.go ├── debugger.go ├── handlers.go ├── keyboard.go ├── log.go ├── misc_dbg.go ├── server │ └── dbg_rpc.go ├── states │ └── ss_dbg.go ├── test │ ├── integration_test.go │ └── remote │ │ └── integration_remote_test.go ├── testdata │ └── am-dbg-sim.gob.br ├── tree.go ├── ui.go ├── utils.go └── utils_test.go ├── generator ├── cli │ └── cli.go ├── grafana.go ├── schema.go ├── schema_test.go └── states │ └── ss_generator.go ├── repl ├── cli_repl.go ├── misc_repl.go ├── repl.go └── states │ └── ss_repl.go └── visualizer ├── d2.go └── visualizer.go /.dockerignore: -------------------------------------------------------------------------------- 1 | assets* 2 | 3 | # Ignore the .git directory 4 | .git 5 | 6 | # Ignore the node_modules directory 7 | node_modules 8 | 9 | # Ignore temporary files 10 | *.log 11 | *.tmp 12 | 13 | # Ignore build output directories 14 | dist 15 | build 16 | 17 | # Ignore Dockerfile and docker-compose.yml 18 | Dockerfile 19 | docker-compose.yml 20 | 21 | # Ignore Go build artifacts 22 | *.exe 23 | *.exe~ 24 | *.dll 25 | *.so 26 | *.dylib 27 | *.test 28 | *.out 29 | 30 | # Ignore IDE/editor specific files 31 | .vscode 32 | .idea 33 | *.swp 34 | *~ 35 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.go] 4 | indent_style = tab 5 | indent_size = 2 6 | max_line_length = 80 7 | 8 | [*.md] 9 | indent_style = space 10 | indent_size = 4 11 | max_line_length = 120 12 | 13 | [*.{yml|yaml}] 14 | indent_style = space 15 | indent_size = 4 16 | max_line_length = 120 17 | -------------------------------------------------------------------------------- /.github/BUMP: -------------------------------------------------------------------------------- 1 | 30 -------------------------------------------------------------------------------- /.github/workflows/go-helpers.yml: -------------------------------------------------------------------------------- 1 | name: Go Test /pkg/helpers 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | go-version: [ '1.23' ] 12 | 13 | steps: 14 | # Get values for cache paths to be used in later steps 15 | - id: go-cache-paths 16 | run: | 17 | echo "::set-output name=go-build::$(go env GOCACHE)" 18 | echo "::set-output name=go-mod::$(go env GOMODCACHE)" 19 | 20 | # set up 21 | - uses: actions/checkout@v4 22 | - uses: ruby/setup-ruby@v1 23 | with: 24 | ruby-version: '3.3' 25 | - uses: actions/setup-go@v4 26 | with: 27 | go-version: ${{ matrix.go-version }} 28 | 29 | # Cache 30 | - name: Clear cache directory first before trying to restore from cache 31 | run: sudo rm -rf $(go env GOMODCACHE) && sudo rm -rf $(go env GOCACHE) 32 | shell: bash 33 | - name: Go Build Cache 34 | uses: actions/cache@v4 35 | with: 36 | path: ${{ steps.go-cache-paths.outputs.go-build }} 37 | key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} 38 | - name: Go Mod Cache 39 | uses: actions/cache@v4 40 | with: 41 | path: ${{ steps.go-cache-paths.outputs.go-mod }} 42 | key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} 43 | - name: Task Cache 44 | uses: actions/cache@v4 45 | with: 46 | path: ~/.local/bin/task 47 | key: ${{ runner.os }} 48 | 49 | # deps 50 | - name: Install Task 51 | run: ./scripts/dep-taskfile.sh 52 | - name: Install dependencies 53 | run: task install-deps 54 | 55 | # run 56 | 57 | - name: Test /pkg/helpers 58 | run: task test-path -- ./pkg/helpers/... 59 | -------------------------------------------------------------------------------- /.github/workflows/go-history.yml: -------------------------------------------------------------------------------- 1 | name: Go Test /pkg/history 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | go-version: [ '1.23' ] 12 | 13 | steps: 14 | # Get values for cache paths to be used in later steps 15 | - id: go-cache-paths 16 | run: | 17 | echo "::set-output name=go-build::$(go env GOCACHE)" 18 | echo "::set-output name=go-mod::$(go env GOMODCACHE)" 19 | 20 | # set up 21 | - uses: actions/checkout@v4 22 | - uses: ruby/setup-ruby@v1 23 | with: 24 | ruby-version: '3.3' 25 | - uses: actions/setup-go@v4 26 | with: 27 | go-version: ${{ matrix.go-version }} 28 | 29 | # Cache 30 | - name: Clear cache directory first before trying to restore from cache 31 | run: sudo rm -rf $(go env GOMODCACHE) && sudo rm -rf $(go env GOCACHE) 32 | shell: bash 33 | - name: Go Build Cache 34 | uses: actions/cache@v4 35 | with: 36 | path: ${{ steps.go-cache-paths.outputs.go-build }} 37 | key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} 38 | - name: Go Mod Cache 39 | uses: actions/cache@v4 40 | with: 41 | path: ${{ steps.go-cache-paths.outputs.go-mod }} 42 | key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} 43 | - name: Task Cache 44 | uses: actions/cache@v4 45 | with: 46 | path: ~/.local/bin/task 47 | key: ${{ runner.os }} 48 | 49 | # deps 50 | - name: Install Task 51 | run: ./scripts/dep-taskfile.sh 52 | - name: Install dependencies 53 | run: task install-deps 54 | 55 | # run 56 | 57 | - name: Test /pkg/history 58 | run: task test-path -- ./pkg/history/... 59 | -------------------------------------------------------------------------------- /.github/workflows/go-machine.yml: -------------------------------------------------------------------------------- 1 | name: Go Test /pkg/machine 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | go-version: [ '1.23' ] 12 | 13 | steps: 14 | # Get values for cache paths to be used in later steps 15 | - id: go-cache-paths 16 | run: | 17 | echo "::set-output name=go-build::$(go env GOCACHE)" 18 | echo "::set-output name=go-mod::$(go env GOMODCACHE)" 19 | 20 | # set up 21 | - uses: actions/checkout@v4 22 | - uses: ruby/setup-ruby@v1 23 | with: 24 | ruby-version: '3.3' 25 | - uses: actions/setup-go@v4 26 | with: 27 | go-version: ${{ matrix.go-version }} 28 | 29 | # Cache 30 | - name: Clear cache directory first before trying to restore from cache 31 | run: sudo rm -rf $(go env GOMODCACHE) && sudo rm -rf $(go env GOCACHE) 32 | shell: bash 33 | - name: Go Build Cache 34 | uses: actions/cache@v4 35 | with: 36 | path: ${{ steps.go-cache-paths.outputs.go-build }} 37 | key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} 38 | - name: Go Mod Cache 39 | uses: actions/cache@v4 40 | with: 41 | path: ${{ steps.go-cache-paths.outputs.go-mod }} 42 | key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} 43 | - name: Task Cache 44 | uses: actions/cache@v4 45 | with: 46 | path: ~/.local/bin/task 47 | key: ${{ runner.os }} 48 | 49 | # deps 50 | - name: Install Task 51 | run: ./scripts/dep-taskfile.sh 52 | - name: Install dependencies 53 | run: task install-deps 54 | 55 | # run 56 | 57 | - name: Test /pkg/machine 58 | run: task test-path -- ./pkg/machine/... 59 | -------------------------------------------------------------------------------- /.github/workflows/go-node.yml: -------------------------------------------------------------------------------- 1 | name: Go Test /pkg/node 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | go-version: [ '1.23' ] 12 | 13 | steps: 14 | # Get values for cache paths to be used in later steps 15 | - id: go-cache-paths 16 | run: | 17 | echo "::set-output name=go-build::$(go env GOCACHE)" 18 | echo "::set-output name=go-mod::$(go env GOMODCACHE)" 19 | 20 | # set up 21 | - uses: actions/checkout@v4 22 | - uses: ruby/setup-ruby@v1 23 | with: 24 | ruby-version: '3.3' 25 | - uses: actions/setup-go@v4 26 | with: 27 | go-version: ${{ matrix.go-version }} 28 | 29 | # Cache 30 | - name: Clear cache directory first before trying to restore from cache 31 | run: sudo rm -rf $(go env GOMODCACHE) && sudo rm -rf $(go env GOCACHE) 32 | shell: bash 33 | - name: Go Build Cache 34 | uses: actions/cache@v4 35 | with: 36 | path: ${{ steps.go-cache-paths.outputs.go-build }} 37 | key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} 38 | - name: Go Mod Cache 39 | uses: actions/cache@v4 40 | with: 41 | path: ${{ steps.go-cache-paths.outputs.go-mod }} 42 | key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} 43 | - name: Task Cache 44 | uses: actions/cache@v4 45 | with: 46 | path: ~/.local/bin/task 47 | key: ${{ runner.os }} 48 | 49 | # deps 50 | - name: Install Task 51 | run: ./scripts/dep-taskfile.sh 52 | - name: Install dependencies 53 | run: task install-deps 54 | 55 | # run 56 | 57 | - name: Test /pkg/node 58 | run: task test-path -- ./pkg/node/... 59 | -------------------------------------------------------------------------------- /.github/workflows/go-pubsub.yml: -------------------------------------------------------------------------------- 1 | name: Go Test /pkg/pubsub 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | go-version: [ '1.23' ] 12 | 13 | steps: 14 | # Get values for cache paths to be used in later steps 15 | - id: go-cache-paths 16 | run: | 17 | echo "::set-output name=go-build::$(go env GOCACHE)" 18 | echo "::set-output name=go-mod::$(go env GOMODCACHE)" 19 | 20 | # set up 21 | - uses: actions/checkout@v4 22 | - uses: ruby/setup-ruby@v1 23 | with: 24 | ruby-version: '3.3' 25 | - uses: actions/setup-go@v4 26 | with: 27 | go-version: ${{ matrix.go-version }} 28 | 29 | # Cache 30 | - name: Clear cache directory first before trying to restore from cache 31 | run: sudo rm -rf $(go env GOMODCACHE) && sudo rm -rf $(go env GOCACHE) 32 | shell: bash 33 | - name: Go Build Cache 34 | uses: actions/cache@v4 35 | with: 36 | path: ${{ steps.go-cache-paths.outputs.go-build }} 37 | key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} 38 | - name: Go Mod Cache 39 | uses: actions/cache@v4 40 | with: 41 | path: ${{ steps.go-cache-paths.outputs.go-mod }} 42 | key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} 43 | - name: Task Cache 44 | uses: actions/cache@v4 45 | with: 46 | path: ~/.local/bin/task 47 | key: ${{ runner.os }} 48 | 49 | # deps 50 | - name: Install Task 51 | run: ./scripts/dep-taskfile.sh 52 | - name: Install dependencies 53 | run: task install-deps 54 | 55 | # run 56 | 57 | - name: Test /pkg/pubsub 58 | run: task test-path -- ./pkg/pubsub/... 59 | -------------------------------------------------------------------------------- /.github/workflows/go-rpc.yml: -------------------------------------------------------------------------------- 1 | name: Go Test /pkg/rpc 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | go-version: [ '1.23' ] 12 | 13 | steps: 14 | # Get values for cache paths to be used in later steps 15 | - id: go-cache-paths 16 | run: | 17 | echo "::set-output name=go-build::$(go env GOCACHE)" 18 | echo "::set-output name=go-mod::$(go env GOMODCACHE)" 19 | 20 | # set up 21 | - uses: actions/checkout@v4 22 | - uses: ruby/setup-ruby@v1 23 | with: 24 | ruby-version: '3.3' 25 | - uses: actions/setup-go@v4 26 | with: 27 | go-version: ${{ matrix.go-version }} 28 | 29 | # Cache 30 | - name: Clear cache directory first before trying to restore from cache 31 | run: sudo rm -rf $(go env GOMODCACHE) && sudo rm -rf $(go env GOCACHE) 32 | shell: bash 33 | - name: Go Build Cache 34 | uses: actions/cache@v4 35 | with: 36 | path: ${{ steps.go-cache-paths.outputs.go-build }} 37 | key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} 38 | - name: Go Mod Cache 39 | uses: actions/cache@v4 40 | with: 41 | path: ${{ steps.go-cache-paths.outputs.go-mod }} 42 | key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} 43 | - name: Task Cache 44 | uses: actions/cache@v4 45 | with: 46 | path: ~/.local/bin/task 47 | key: ${{ runner.os }} 48 | 49 | # deps 50 | - name: Install Task 51 | run: ./scripts/dep-taskfile.sh 52 | - name: Install dependencies 53 | run: task install-deps 54 | 55 | # run 56 | 57 | - name: Test /pkg/rpc 58 | run: task test-path -- ./pkg/rpc/... 59 | -------------------------------------------------------------------------------- /.github/workflows/go-states.yml: -------------------------------------------------------------------------------- 1 | name: Go Test /pkg/states 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | go-version: [ '1.23' ] 12 | 13 | steps: 14 | # Get values for cache paths to be used in later steps 15 | - id: go-cache-paths 16 | run: | 17 | echo "::set-output name=go-build::$(go env GOCACHE)" 18 | echo "::set-output name=go-mod::$(go env GOMODCACHE)" 19 | 20 | # set up 21 | - uses: actions/checkout@v4 22 | - uses: ruby/setup-ruby@v1 23 | with: 24 | ruby-version: '3.3' 25 | - uses: actions/setup-go@v4 26 | with: 27 | go-version: ${{ matrix.go-version }} 28 | 29 | # Cache 30 | - name: Clear cache directory first before trying to restore from cache 31 | run: sudo rm -rf $(go env GOMODCACHE) && sudo rm -rf $(go env GOCACHE) 32 | shell: bash 33 | - name: Go Build Cache 34 | uses: actions/cache@v4 35 | with: 36 | path: ${{ steps.go-cache-paths.outputs.go-build }} 37 | key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} 38 | - name: Go Mod Cache 39 | uses: actions/cache@v4 40 | with: 41 | path: ${{ steps.go-cache-paths.outputs.go-mod }} 42 | key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} 43 | - name: Task Cache 44 | uses: actions/cache@v4 45 | with: 46 | path: ~/.local/bin/task 47 | key: ${{ runner.os }} 48 | 49 | # deps 50 | - name: Install Task 51 | run: ./scripts/dep-taskfile.sh 52 | - name: Install dependencies 53 | run: task install-deps 54 | 55 | # run 56 | 57 | - name: Test /pkg/states 58 | run: task test-path -- ./pkg/states/... 59 | -------------------------------------------------------------------------------- /.github/workflows/go-telemetry.yml: -------------------------------------------------------------------------------- 1 | name: Go Test /pkg/telemetry 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | go-version: [ '1.23' ] 12 | 13 | steps: 14 | # Get values for cache paths to be used in later steps 15 | - id: go-cache-paths 16 | run: | 17 | echo "::set-output name=go-build::$(go env GOCACHE)" 18 | echo "::set-output name=go-mod::$(go env GOMODCACHE)" 19 | 20 | # set up 21 | - uses: actions/checkout@v4 22 | - uses: ruby/setup-ruby@v1 23 | with: 24 | ruby-version: '3.3' 25 | - uses: actions/setup-go@v4 26 | with: 27 | go-version: ${{ matrix.go-version }} 28 | 29 | # Cache 30 | - name: Clear cache directory first before trying to restore from cache 31 | run: sudo rm -rf $(go env GOMODCACHE) && sudo rm -rf $(go env GOCACHE) 32 | shell: bash 33 | - name: Go Build Cache 34 | uses: actions/cache@v4 35 | with: 36 | path: ${{ steps.go-cache-paths.outputs.go-build }} 37 | key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} 38 | - name: Go Mod Cache 39 | uses: actions/cache@v4 40 | with: 41 | path: ${{ steps.go-cache-paths.outputs.go-mod }} 42 | key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} 43 | - name: Task Cache 44 | uses: actions/cache@v4 45 | with: 46 | path: ~/.local/bin/task 47 | key: ${{ runner.os }} 48 | 49 | # deps 50 | - name: Install Task 51 | run: ./scripts/dep-taskfile.sh 52 | - name: Install dependencies 53 | run: task install-deps 54 | 55 | # run 56 | 57 | - name: Test /pkg/telemetry 58 | run: task test-path -- ./pkg/telemetry/... 59 | -------------------------------------------------------------------------------- /.github/workflows/go-tools.yml: -------------------------------------------------------------------------------- 1 | name: Go Test /tools 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | go-version: [ '1.23' ] 12 | 13 | steps: 14 | # Get values for cache paths to be used in later steps 15 | - id: go-cache-paths 16 | run: | 17 | echo "::set-output name=go-build::$(go env GOCACHE)" 18 | echo "::set-output name=go-mod::$(go env GOMODCACHE)" 19 | 20 | # set up 21 | - uses: actions/checkout@v4 22 | - uses: ruby/setup-ruby@v1 23 | with: 24 | ruby-version: '3.3' 25 | - uses: actions/setup-go@v4 26 | with: 27 | go-version: ${{ matrix.go-version }} 28 | 29 | # Cache 30 | - name: Clear cache directory first before trying to restore from cache 31 | run: sudo rm -rf $(go env GOMODCACHE) && sudo rm -rf $(go env GOCACHE) 32 | shell: bash 33 | - name: Go Build Cache 34 | uses: actions/cache@v4 35 | with: 36 | path: ${{ steps.go-cache-paths.outputs.go-build }} 37 | key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} 38 | - name: Go Mod Cache 39 | uses: actions/cache@v4 40 | with: 41 | path: ${{ steps.go-cache-paths.outputs.go-mod }} 42 | key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} 43 | - name: Task Cache 44 | uses: actions/cache@v4 45 | with: 46 | path: ~/.local/bin/task 47 | key: ${{ runner.os }} 48 | 49 | # deps 50 | - name: Install Task 51 | run: ./scripts/dep-taskfile.sh 52 | - name: Install dependencies 53 | run: task install-deps 54 | 55 | # run 56 | 57 | - name: Test /tools 58 | run: task test-path -- ./tools/... 59 | -------------------------------------------------------------------------------- /.github/workflows/linter.yml: -------------------------------------------------------------------------------- 1 | name: Linter 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | # if: false # Disable this job 9 | 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | go-version: [ '1.23' ] 14 | 15 | steps: 16 | # Get values for cache paths to be used in later steps 17 | - id: go-cache-paths 18 | run: | 19 | echo "::set-output name=go-build::$(go env GOCACHE)" 20 | echo "::set-output name=go-mod::$(go env GOMODCACHE)" 21 | 22 | # set up 23 | - uses: actions/checkout@v4 24 | - uses: ruby/setup-ruby@v1 25 | with: 26 | ruby-version: '3.3' 27 | - uses: actions/setup-go@v4 28 | with: 29 | go-version: ${{ matrix.go-version }} 30 | 31 | # Cache 32 | - name: Clear cache directory first before trying to restore from cache 33 | run: sudo rm -rf $(go env GOMODCACHE) && sudo rm -rf $(go env GOCACHE) 34 | shell: bash 35 | - name: Go Build Cache 36 | uses: actions/cache@v4 37 | with: 38 | path: ${{ steps.go-cache-paths.outputs.go-build }} 39 | key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} 40 | - name: Go Mod Cache 41 | uses: actions/cache@v4 42 | with: 43 | path: ${{ steps.go-cache-paths.outputs.go-mod }} 44 | key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} 45 | - name: Task Cache 46 | uses: actions/cache@v4 47 | with: 48 | path: ~/.local/bin/task 49 | key: ${{ runner.os }} 50 | 51 | # deps 52 | - name: Install Task 53 | run: ./scripts/dep-taskfile.sh 54 | - name: Install dependencies 55 | run: task install-deps 56 | 57 | # run 58 | - name: Run linters 59 | run: task lint 60 | -------------------------------------------------------------------------------- /.github/workflows/releaser.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - 16 | name: Checkout 17 | uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | - 21 | name: Set up Go 22 | uses: actions/setup-go@v4 23 | - 24 | name: Run GoReleaser 25 | uses: goreleaser/goreleaser-action@v5 26 | with: 27 | distribution: goreleaser 28 | version: latest 29 | args: release --clean 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github_changelog_generator: -------------------------------------------------------------------------------- 1 | # https://github.com/github-changelog-generator/github-changelog-generator/wiki/Advanced-change-log-generation-examples 2 | issues=false 3 | compare-link=false 4 | usernames-as-github-logins=true 5 | pr-label= 6 | since-tag=v0.1.0 7 | user=pancsta 8 | project=asyncmachine-go 9 | unreleased=true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Jetbrains IDEs 2 | .idea 3 | 4 | # Vim 5 | *.swp 6 | *.swn 7 | *.swo 8 | 9 | # MacOS system file 10 | .DS_Store 11 | 12 | # Environment variable files 13 | .env 14 | 15 | # PEM files 16 | *.pem 17 | 18 | # Binaries for programs and plugins 19 | *.exe 20 | *.exe~ 21 | *.dll 22 | *.so 23 | *.dylib 24 | 25 | # Binaries compiled by Go 26 | build/ 27 | 28 | # Test binary, built with `go test -c` 29 | *.test 30 | 31 | # Output of the go coverage tool 32 | *.new 33 | *.old 34 | *.out 35 | 36 | # Dependency directories 37 | vendor/ 38 | 39 | # Redis 40 | *.rdb 41 | 42 | # Go workspace file 43 | go.work 44 | 45 | # local dev 46 | /.dev 47 | /dist 48 | /tools/cmd/am-dbg/am-dbg 49 | *.log 50 | /*.gob 51 | /assets/*.cast.yml 52 | 53 | /tmp 54 | _py 55 | /.run 56 | /*.bz2 57 | /*.br 58 | /mem.* 59 | /cpu.* 60 | /.local 61 | /assets/video.gif 62 | /*.svg 63 | /assets/*.pdf 64 | /*.png 65 | /*.prof 66 | /am-dbg-ssh 67 | /built 68 | /log.md 69 | /coverage*.txt 70 | /tests*.txt 71 | /assets 72 | /__* 73 | /trace* 74 | *.mmd 75 | /demo 76 | /am-dbg.txt 77 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "assets"] 2 | path = assets 3 | url = https://github.com/pancsta/assets.git 4 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | exclude-files: 3 | - "examples/.*" 4 | - "scripts/.*" 5 | 6 | linters: 7 | enable: 8 | - lll 9 | 10 | linters-settings: 11 | lll: 12 | line-length: 80 13 | tab-width: 2 14 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # .goreleaser.yml 2 | 3 | builds: 4 | - 5 | id: "am-dbg" 6 | env: 7 | - CGO_ENABLED=0 8 | - GO111MODULE=on 9 | goos: 10 | - linux 11 | - darwin 12 | - windows 13 | goarch: 14 | - amd64 15 | - arm64 16 | main: ./tools/cmd/am-dbg 17 | binary: am-dbg 18 | 19 | - 20 | id: "am-dbg-ssh" 21 | env: 22 | - CGO_ENABLED=0 23 | - GO111MODULE=on 24 | goos: 25 | - linux 26 | - darwin 27 | - windows 28 | goarch: 29 | - amd64 30 | - arm64 31 | main: ./tools/cmd/am-dbg-ssh 32 | binary: am-dbg-ssh 33 | 34 | - 35 | id: "arpc" 36 | env: 37 | - CGO_ENABLED=0 38 | - GO111MODULE=on 39 | goos: 40 | - linux 41 | - darwin 42 | - windows 43 | goarch: 44 | - amd64 45 | - arm64 46 | main: ./tools/cmd/arpc 47 | binary: arpc 48 | 49 | archives: 50 | - 51 | id: "am-dbg" 52 | format: tar.gz 53 | name_template: "am-dbg_{{ .Version }}_{{ .Os }}_{{ .Arch }}" 54 | # dont add readmes etc 55 | files: [""] 56 | builds: 57 | - am-dbg 58 | 59 | - 60 | id: "am-dbg-ssh" 61 | format: tar.gz 62 | name_template: "am-dbg-ssh_{{ .Version }}_{{ .Os }}_{{ .Arch }}" 63 | # dont add readmes etc 64 | files: [""] 65 | builds: 66 | - am-dbg 67 | - am-dbg-ssh 68 | 69 | - 70 | id: "arpc" 71 | format: tar.gz 72 | name_template: "arpc_{{ .Version }}_{{ .Os }}_{{ .Arch }}" 73 | # dont add readmes etc 74 | files: [""] 75 | builds: 76 | - arpc 77 | 78 | checksum: 79 | name_template: 'checksums.txt' 80 | 81 | release: 82 | github: 83 | owner: pancsta 84 | name: asyncmachine-go 85 | draft: true 86 | replace_existing_draft: true 87 | 88 | changelog: 89 | sort: asc 90 | filters: 91 | exclude: 92 | - '^docs:' 93 | - '^test:' 94 | - '^chore:' 95 | - '^refactor:' 96 | - '^style:' 97 | - '^ci:' 98 | - '^perf:' 99 | - '^revert:' 100 | snapshot: 101 | name_template: "{{ .Tag }}-next" -------------------------------------------------------------------------------- /BREAKING.md: -------------------------------------------------------------------------------- 1 | # Breaking API changes 2 | 3 | Only `pkg/machine` and `pkg/states` adhere to semver. Semver of other packages is not guaranteed at the moment. 4 | 5 | ## v0.11 6 | 7 | - `am.Struct` is now `am.Schema` 8 | - `Machine.GetStruct()` is now `Machine.Schema()` 9 | - `am.StructMerge()` is now `am.SchemaMerge()` 10 | - `Tracer.StructChange()` is now `Tracer.SchemaChange()` 11 | - `Machine.WhenTicksEq()` is now `Machine.WhenTime1()` 12 | 13 | ## v0.10 14 | 15 | - `FooBar()` handlers get executed later and more often 16 | - `FooAny()`, `AnyFoo()` handlers have been removed 17 | - `AnyAny()` is now `AnyEnter()` 18 | 19 | ## v0.9 20 | 21 | - `Event.Machine` is now `Event.Machine()` 22 | - `Machine.RegisterDisposalHandler(func())` is now `HandleDispose(func(id, ctx))` 23 | - `Step.FromState` is now `Step.GetFromState()` 24 | - `Step.ToState` is now `Step.GetToState()` 25 | - `Step.Data` is now `Step.RelType` 26 | 27 | ## v0.8 28 | 29 | - `Machine.ID` is now `Id()` 30 | - `Machine.Tracers` is now `Tracers() Tracers` 31 | - `Machine.LogID` is now `GetLogId() bool` 32 | - `Machine.Switch(ss... string)` is now `Switch(states S)` 33 | - `Machine.StatesVerified` is now `StatesVerified()` 34 | - `Machine.ParentID` is now `ParentId()` 35 | - `Transition.StatesBefore` is now `StatesBefore()` 36 | - `Transition.TargetStates` is now `TargetStates()` 37 | - `Tracer.TransitionInit` now returns an optional `Context` 38 | - `Machine.Ctx` is now `Ctx()` 39 | 40 | ## v0.7 41 | 42 | - `Machine.PrintExceptions` is now `Machine.LogStackTrace` 43 | - `Machine.Resolver` is now `Machine.Resolver()` 44 | - `Machine.StateNames` is now `Machine.StateNames()` 45 | - `Machine.Transition` is now `Machine.Transition()` 46 | - `Machine.Err` is now `Machine.Err()` 47 | - `Machine.AddErrStr()` has been removed 48 | - `Machine.AddErr(error)` is now `Machine.AddErr(error, Args)` 49 | - `Machine.AddErrState(string, error)` is now `Machine.AddErrState(state, error, Args)` 50 | - `Machine.WhenTicksEq()` now accepts `uint64` 51 | - `Machine.IsClock()` and `Machine.Clock()` are now `Machine.Time()` 52 | - `Machine.OnEvent()` has been removed 53 | - `Machine.DuringTransition()` is now `Machine.Transition()` 54 | - `Machine.SetTestLogger()` is now `Machine.SetLoggerSimple()` 55 | - `Machine.HasStateChanged()` is now `Machine.IsClock()` 56 | - `Machine.HasStateChangedSince()` is now `Machine.IsTime()` 57 | - `Machine.Clocks()` is now `Machine.Clock()` 58 | - `Machine.Export()` and `Machine.Import()` now use `am.Serialized` 59 | - `Opts.DontPrintExceptions` is now`Opts.DontLogStackTrace` 60 | - `Transition.ClocksBefore` is now `Transition.ClockBefore()` 61 | - `Transition.ClocksAfter` is now `Transition.ClockAfter()` 62 | - `Transition.TAfter` is now `Transition.TimeAfter` 63 | - `Transition.IsCompleted` is now `Transition.IsCompleted()` 64 | - `T` is now `Time` 65 | - `Clocks` is now `Clock` 66 | - `Event*` enum has been removed 67 | - `SMerge` is now `SAdd` 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Kentaro Hibino 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. -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # Roadmap 2 | 3 | - negotiation testers (eg `CanAdd`) 4 | - more helpers for queue and history traversal 5 | - handlers for RPC workers 6 | - DB-based scheduler 7 | - NATS integration 8 | - go1.22 traces 9 | - inference 10 | - dynamic state schemas 11 | - optimizations 12 | - tutorials 13 | 14 | See also [issues](https://github.com/pancsta/asyncmachine-go/issues). 15 | -------------------------------------------------------------------------------- /codecov.yaml: -------------------------------------------------------------------------------- 1 | component_management: 2 | 3 | default_rules: 4 | statuses: 5 | - type: project 6 | target: auto 7 | branches: 8 | - "!main" 9 | 10 | individual_components: 11 | - component_id: machine 12 | name: pkg/machine 13 | paths: 14 | - pkg/machine/** 15 | - component_id: helpers 16 | name: pkg/helpers 17 | paths: 18 | - pkg/helpers/** 19 | - component_id: history 20 | name: pkg/history 21 | paths: 22 | - pkg/history/** 23 | - component_id: node 24 | name: pkg/node 25 | paths: 26 | - pkg/node/** 27 | - component_id: pubsub 28 | name: pkg/pubsub 29 | paths: 30 | - pkg/pubsub/** 31 | - component_id: rpc 32 | name: pkg/rpc 33 | paths: 34 | - pkg/rpc/** 35 | - component_id: states 36 | name: pkg/states 37 | paths: 38 | - pkg/states/** 39 | - component_id: telemetry 40 | name: pkg/telemetry 41 | paths: 42 | - pkg/telemetry/** 43 | 44 | - component_id: tools 45 | name: tools 46 | paths: 47 | - tools/** 48 | 49 | - component_id: internal 50 | name: internal 51 | paths: 52 | - internal/** -------------------------------------------------------------------------------- /config/.mdl_style.rb: -------------------------------------------------------------------------------- 1 | all 2 | rule 'MD013', :line_length => 120 3 | # list style 4 | rule 'MD029', :style => :ordered 5 | # dollar sign 6 | exclude_rule 'MD014' 7 | # HTML blocks 8 | exclude_rule 'MD033' 9 | # block quotes 10 | exclude_rule 'MD028' 11 | # first line H1 12 | exclude_rule 'MD002' 13 | # first line H1 14 | exclude_rule 'MD041' 15 | # ??? 16 | exclude_rule 'MD007' 17 | # header question mark 18 | exclude_rule 'MD026' 19 | # Header levels should only increment by one level at a time 20 | # not compatible with hashtags 21 | exclude_rule 'MD001' 22 | # Spaces inside emphasis markers (buggy) 23 | exclude_rule 'MD037' 24 | -------------------------------------------------------------------------------- /config/.mdlrc: -------------------------------------------------------------------------------- 1 | style "#{File.dirname(__FILE__)}/.mdl_style.rb" -------------------------------------------------------------------------------- /config/cli/debug-am-dbg.txt: -------------------------------------------------------------------------------- 1 | --am-dbg-addr=localhost:9913 2 | --log-level=2 -------------------------------------------------------------------------------- /config/dashboards/dash-1x1.kdl: -------------------------------------------------------------------------------- 1 | layout { 2 | pane split_direction="horizontal" { 3 | pane size="50%" command="am-dbg" { 4 | args "-l" "localhost:6832" "--view-narrow" "--view-timelines" "0" "--tail" "--view-rain" "1" "--view" "tree-matrix" 5 | } 6 | 7 | pane size="50%" command="sh" { 8 | args "-c" "sleep 2 && am-dbg --dir tmp -l localhost:6831 --fwd-data localhost:6832 --graph 3 --tail" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /config/dashboards/dash-2x1.kdl: -------------------------------------------------------------------------------- 1 | layout { 2 | pane split_direction="horizontal" { 3 | pane size="50%" split_direction="vertical" { 4 | pane size="50%" command="am-dbg" { 5 | args "-l" "localhost:6832" "--view-narrow" "--view-timelines" "0" "--tail" "--view-rain" "1" "--view" "matrix" 6 | } 7 | pane size="50%" command="am-dbg" { 8 | args "-l" "localhost:6833" "--view-narrow" "--view-timelines" "0" "--tail" 9 | } 10 | } 11 | 12 | pane size="50%" command="sh" { 13 | args "-c" "sleep 2 && am-dbg --dir tmp -l localhost:6831 --fwd-data localhost:6832,localhost:6833 --graph 3 --tail" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /config/dashboards/dash-2x1x1.kdl: -------------------------------------------------------------------------------- 1 | layout { 2 | pane split_direction="vertical" { 3 | pane size="50%" split_direction="horizontal" { 4 | pane size="50%" split_direction="vertical" { 5 | pane size="50%" command="sh" { 6 | args "-c" "task am-dbg -- -l localhost:6832 --view-narrow --view-timelines 0 --view matrix --tail" 7 | } 8 | pane size="50%" command="sh" { 9 | args "-c" "task am-dbg -- -l localhost:6833 --view-narrow --view-timelines 0 --view-rain --view matrix --tail" 10 | } 11 | } 12 | pane size="50%" command="sh" { 13 | args "-c" "task am-dbg -- -l localhost:6834 --view-narrow --view-timelines 0 --tail --view-timelines 1 --view-reader" 14 | } 15 | } 16 | 17 | pane size="50%" command="sh" { 18 | args "-c" "sleep 2 && task am-dbg -- --dir tmp -l localhost:6831 --fwd-data localhost:6832,localhost:6833,localhost:6834 --diagrams 3 --tail --output-clients" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /config/env/debug-node.env: -------------------------------------------------------------------------------- 1 | AM_NODE_LOG_SUPERVISOR=1 2 | AM_NODE_LOG_CLIENT=1 3 | -------------------------------------------------------------------------------- /config/env/debug-pubsub.env: -------------------------------------------------------------------------------- 1 | AM_PUBSUB_LOG=1 2 | AM_PUBSUB_DBG=1 3 | -------------------------------------------------------------------------------- /config/env/debug-rpc.env: -------------------------------------------------------------------------------- 1 | AM_RPC_LOG_SERVER=1 2 | AM_RPC_LOG_CLIENT=1 3 | AM_RPC_LOG_MUX=1 4 | AM_RPC_DBG=1 5 | AM_REPL_ADDR=1 6 | AM_REPL_ADDR_DIR=tmp 7 | -------------------------------------------------------------------------------- /config/env/debug-simple.env: -------------------------------------------------------------------------------- 1 | AM_DEBUG=1 2 | -------------------------------------------------------------------------------- /config/env/debug-telemetry-dbg.env: -------------------------------------------------------------------------------- 1 | AM_DBG_ADDR=localhost:6831 2 | AM_LOG=2 3 | AM_HEALTHCHECK=1 4 | -------------------------------------------------------------------------------- /config/env/debug-tests.env: -------------------------------------------------------------------------------- 1 | AM_DEBUG=1 2 | AM_DBG_ADDR=localhost:6831 3 | AM_LOG=2 4 | AM_TEST_DEBUG=1 5 | AM_DETECT_EVAL=1 -------------------------------------------------------------------------------- /config/env/tests-remote.env: -------------------------------------------------------------------------------- 1 | AM_DBG_WORKER_RPC_ADDR=localhost:53480 2 | AM_DBG_WORKER_TELEMETRY_ADDR=localhost:53470 3 | -------------------------------------------------------------------------------- /config/terminalizer.yml: -------------------------------------------------------------------------------- 1 | # Specify a command to be executed 2 | # like `/bin/bash -l`, `ls`, or any other commands 3 | # the default is bash for Linux 4 | # or powershell.exe for Windows 5 | command: go run ./internal/cmd/am-dbg-video 6 | 7 | # Specify the current working directory path 8 | # the default is the current working directory path 9 | cwd: null 10 | 11 | # Export additional ENV variables 12 | env: 13 | RECORDING: true 14 | 15 | # Explicitly set the number of columns 16 | # or use `auto` to take the current 17 | # number of columns of your shell 18 | cols: 135 19 | 20 | # Explicitly set the number of rows 21 | # or use `auto` to take the current 22 | # number of rows of your shell 23 | rows: 40 24 | 25 | # Amount of times to repeat GIF 26 | # If value is -1, play once 27 | # If value is 0, loop indefinitely 28 | # If value is a positive number, loop n times 29 | repeat: 0 30 | 31 | # Quality 32 | # 1 - 100 33 | quality: 100 34 | 35 | # Delay between frames in ms 36 | # If the value is `auto` use the actual recording delays 37 | frameDelay: auto 38 | 39 | # Maximum delay between frames in ms 40 | # Ignored if the `frameDelay` isn't set to `auto` 41 | # Set to `auto` to prevent limiting the max idle time 42 | maxIdleTime: 2000 43 | 44 | # The surrounding frame box 45 | # The `type` can be null, window, floating, or solid` 46 | # To hide the title use the value null 47 | # Don't forget to add a backgroundColor style with a null as type 48 | frameBox: 49 | type: null 50 | title: null 51 | style: 52 | border: 0px black solid 53 | background: #231f20 54 | # boxShadow: none 55 | # margin: 0px 56 | 57 | # Add a watermark image to the rendered gif 58 | # You need to specify an absolute path for 59 | # the image on your machine or a URL, and you can also 60 | # add your own CSS styles 61 | watermark: 62 | imagePath: null 63 | style: 64 | position: absolute 65 | right: 15px 66 | bottom: 15px 67 | width: 100px 68 | opacity: 0.9 69 | 70 | # Cursor style can be one of 71 | # `block`, `underline`, or `bar` 72 | cursorStyle: block 73 | 74 | # Font family 75 | # You can use any font that is installed on your machine 76 | # in CSS-like syntax 77 | fontFamily: "Liberation Mono, Monaco, Lucida Console, Ubuntu Mono, Monospace" 78 | 79 | # The size of the font 80 | fontSize: 10 81 | 82 | # The height of lines 83 | lineHeight: 1 84 | 85 | # The spacing between letters 86 | letterSpacing: 0 87 | 88 | # Theme 89 | theme: 90 | background: "#231f20" 91 | foreground: "#afafaf" 92 | cursor: "#c7c7c7" 93 | black: "#232628" 94 | red: "#fc4384" 95 | green: "#b3e33b" 96 | yellow: "#ffa727" 97 | blue: "#75dff2" 98 | magenta: "#ae89fe" 99 | cyan: "#708387" 100 | white: "#d5d5d0" 101 | brightBlack: "#626566" 102 | brightRed: "#ff7fac" 103 | brightGreen: "#c8ed71" 104 | brightYellow: "#ebdf86" 105 | brightBlue: "#75dff2" 106 | brightMagenta: "#ae89fe" 107 | brightCyan: "#b1c6ca" 108 | brightWhite: "#f9f9f4" 109 | -------------------------------------------------------------------------------- /deploy/web-metrics/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | prometheus: 4 | image: prom/prometheus:v2.37.9 5 | container_name: prometheus 6 | user: root 7 | ports: 8 | - "9090:9090" 9 | command: 10 | - '--config.file=/etc/prometheus/prometheus.yaml' 11 | volumes: 12 | - ./prometheus.yaml:/etc/prometheus/prometheus.yaml:ro 13 | restart: unless-stopped 14 | 15 | grafana: 16 | image: grafana/grafana-oss:latest 17 | container_name: grafana 18 | ports: 19 | - "3000:3000" 20 | volumes: 21 | - grafana_data:/var/lib/grafana 22 | environment: 23 | - GF_PATHS_PROVISIONING=/etc/grafana/provisioning 24 | - GF_AUTH_ANONYMOUS_ENABLED=true 25 | - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin 26 | restart: unless-stopped 27 | #password: root123 28 | entrypoint: 29 | - sh 30 | - -euc 31 | - | 32 | mkdir -p /etc/grafana/provisioning/datasources 33 | cat < /etc/grafana/provisioning/datasources/ds.yaml 34 | apiVersion: 1 35 | datasources: 36 | - name: Prometheus 37 | type: prometheus 38 | access: proxy 39 | orgId: 1 40 | url: http://prometheus:9090 41 | basicAuth: false 42 | isDefault: true 43 | version: 1 44 | editable: false 45 | - name: Loki 46 | type: loki 47 | access: proxy 48 | orgId: 1 49 | url: http://loki:3100 50 | basicAuth: false 51 | isDefault: false 52 | version: 1 53 | editable: false 54 | EOF 55 | /run.sh 56 | 57 | pushgateway: 58 | image: prom/pushgateway 59 | container_name: pushgateway 60 | ports: 61 | - "9091:9091" 62 | restart: unless-stopped 63 | 64 | # TODO update https://docs.google.com/document/d/18B1yTMewRft2N0nW9K-ecVRTt5VaNgnrPTW1eL236t4/edit?tab=t.0#heading=h.5j21da1bep6t 65 | 66 | jaeger: 67 | image: jaegertracing/all-in-one:1.26 68 | container_name: jaeger 69 | environment: 70 | - COLLECTOR_OTLP_ENABLED=true 71 | ports: 72 | - "16686:16686" 73 | - "4317:4317" 74 | restart: unless-stopped 75 | 76 | loki: 77 | image: grafana/loki:3.1.1 78 | container_name: loki 79 | ports: 80 | - "3100:3100" 81 | volumes: 82 | - ./loki.yaml:/etc/loki/local-config.yaml:ro 83 | command: -config.file=/etc/loki/local-config.yaml 84 | restart: unless-stopped 85 | 86 | otel-collector: 87 | image: otel/opentelemetry-collector:0.111.0 88 | container_name: otel-collector 89 | ports: 90 | # - "4317:4317" 91 | - "4318:4318" 92 | - "55681:55681" 93 | volumes: 94 | - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml:ro 95 | command: [ "--config", "/etc/otel-collector-config.yaml" ] 96 | restart: unless-stopped 97 | 98 | volumes: 99 | grafana_data: { } 100 | perses: { } 101 | -------------------------------------------------------------------------------- /deploy/web-metrics/loki.yaml: -------------------------------------------------------------------------------- 1 | limits_config: 2 | allow_structured_metadata: true 3 | otlp_config: 4 | resource_attributes: 5 | attributes_config: 6 | - action: index_label 7 | regex: asyncmachine.id 8 | 9 | auth_enabled: false 10 | 11 | server: 12 | http_listen_port: 3100 13 | 14 | common: 15 | instance_addr: 127.0.0.1 16 | path_prefix: /loki 17 | storage: 18 | filesystem: 19 | chunks_directory: /loki/chunks 20 | rules_directory: /loki/rules 21 | replication_factor: 1 22 | ring: 23 | kvstore: 24 | store: inmemory 25 | 26 | schema_config: 27 | configs: 28 | - from: 2020-10-24 29 | store: tsdb 30 | object_store: filesystem 31 | schema: v13 32 | index: 33 | prefix: index_ 34 | period: 24h 35 | 36 | ruler: 37 | alertmanager_url: http://localhost:9093 38 | 39 | analytics: 40 | reporting_enabled: false 41 | 42 | distributor: 43 | otlp_config: 44 | default_resource_attributes_as_index_labels: 45 | - service.name 46 | - asyncmachine.id -------------------------------------------------------------------------------- /deploy/web-metrics/otel-collector-config.yaml: -------------------------------------------------------------------------------- 1 | receivers: 2 | otlp: 3 | protocols: 4 | grpc: 5 | endpoint: 0.0.0.0:4317 6 | http: 7 | endpoint: 0.0.0.0:4318 8 | cors: 9 | allowed_origins: 10 | - "http://*" 11 | - "https://*" 12 | 13 | processors: 14 | batch: 15 | 16 | exporters: 17 | # debug: 18 | # verbosity: detailed 19 | otlphttp: 20 | endpoint: http://loki:3100/otlp 21 | tls: 22 | insecure: true 23 | 24 | service: 25 | # telemetry: 26 | # logs: 27 | # level: debug 28 | pipelines: 29 | logs: 30 | receivers: [otlp] 31 | processors: [batch] 32 | exporters: [otlphttp] -------------------------------------------------------------------------------- /deploy/web-metrics/prometheus.yaml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 1s 3 | 4 | scrape_configs: 5 | - job_name: 'pushgateway' 6 | honor_labels: true 7 | scrape_interval: 5s 8 | static_configs: 9 | - targets: [ 'pushgateway:9091' ] 10 | -------------------------------------------------------------------------------- /docs/diagrams-examples.md: -------------------------------------------------------------------------------- 1 | # Diagrams from Examples 2 | 3 | ## Tree State Source 4 | 5 | - [origin](/examples/tree_state_source) 6 | 7 | ```mermaid 8 | flowchart BT 9 | Root 10 | Replicant-1 -- aRPC --> Root 11 | Replicant-2 -- aRPC --> Root 12 | 13 | Replicant-1-1 -- aRPC --> Replicant-1 14 | Replicant-1-2 -- aRPC --> Replicant-1 15 | Replicant-1-3 -- aRPC --> Replicant-1 16 | 17 | Replicant-2-1 -- aRPC --> Replicant-2 18 | Replicant-2-2 -- aRPC --> Replicant-2 19 | Replicant-2-3 -- aRPC --> Replicant-2 20 | ``` 21 | 22 | ## Benchmark State Source 23 | 24 | - [origin](/examples/benchmark_state_source) 25 | 26 | ```mermaid 27 | flowchart BT 28 | Root 29 | Replicant-1 -- aRPC --> Root 30 | Replicant-2 -- aRPC --> Root 31 | 32 | Replicant-1-1 -- aRPC --> Replicant-1 33 | Replicant-1-2 -- aRPC --> Replicant-1 34 | Replicant-1-3 -- aRPC --> Replicant-1 35 | 36 | Replicant-2-1 -- aRPC --> Replicant-2 37 | Replicant-2-2 -- aRPC --> Replicant-2 38 | Replicant-2-3 -- aRPC --> Replicant-2 39 | 40 | Caddy[Caddy Load Balancer] 41 | Caddy -- HTTP --> Replicant-1-1 42 | Caddy -- HTTP --> Replicant-1-2 43 | Caddy -- HTTP --> Replicant-1-3 44 | Caddy -- HTTP --> Replicant-2-1 45 | Caddy -- HTTP --> Replicant-2-2 46 | Caddy -- HTTP --> Replicant-2-3 47 | 48 | Benchmark[Benchmark go-wrt] 49 | Benchmark -- HTTP --> Caddy 50 | ``` 51 | -------------------------------------------------------------------------------- /docs/jsonschema/getter_req.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://github.com/pancsta/asyncmachine-go/pkg/integrations/getter-req", 4 | "$ref": "#/$defs/GetterReq", 5 | "$defs": { 6 | "GetterReq": { 7 | "properties": { 8 | "kind": { 9 | "$ref": "#/$defs/Kind", 10 | "description": "The kind of the request." 11 | }, 12 | "time": { 13 | "$ref": "#/$defs/S", 14 | "description": "Request ticks of the passed states" 15 | }, 16 | "time_sum": { 17 | "$ref": "#/$defs/S", 18 | "description": "Request the sum of ticks of the passed states" 19 | }, 20 | "clocks": { 21 | "$ref": "#/$defs/S", 22 | "description": "Request named clocks of the passed states" 23 | }, 24 | "tags": { 25 | "type": "boolean", 26 | "description": "Request the tags of the state machine" 27 | }, 28 | "export": { 29 | "type": "boolean", 30 | "description": "Request an importable version of the state machine" 31 | }, 32 | "id": { 33 | "type": "boolean", 34 | "description": "Request the ID of the state machine" 35 | }, 36 | "parent_id": { 37 | "type": "boolean", 38 | "description": "Request the ID of the parent state machine" 39 | } 40 | }, 41 | "additionalProperties": false, 42 | "type": "object", 43 | "required": [ 44 | "kind" 45 | ], 46 | "description": "GetterReq is a generic request, which results in GetterResp with respective fields filled out." 47 | }, 48 | "Kind": { 49 | "properties": { 50 | "Value": { 51 | "type": "string" 52 | } 53 | }, 54 | "additionalProperties": false, 55 | "type": "object", 56 | "required": [ 57 | "Value" 58 | ] 59 | }, 60 | "S": { 61 | "items": { 62 | "type": "string" 63 | }, 64 | "type": "array" 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /docs/jsonschema/getter_resp.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://github.com/pancsta/asyncmachine-go/pkg/integrations/getter-resp", 4 | "$ref": "#/$defs/GetterResp", 5 | "$defs": { 6 | "Clock": { 7 | "additionalProperties": { 8 | "type": "integer" 9 | }, 10 | "type": "object" 11 | }, 12 | "GetterResp": { 13 | "properties": { 14 | "kind": { 15 | "$ref": "#/$defs/Kind", 16 | "description": "The kind of the response." 17 | }, 18 | "mach_id": { 19 | "type": "string", 20 | "description": "The ID of the state machine." 21 | }, 22 | "time": { 23 | "$ref": "#/$defs/Time", 24 | "description": "The ticks of the passed states" 25 | }, 26 | "time_sum": { 27 | "type": "integer", 28 | "description": "The sum of ticks of the passed states" 29 | }, 30 | "clocks": { 31 | "$ref": "#/$defs/Clock", 32 | "description": "The named clocks of the passed states" 33 | }, 34 | "tags": { 35 | "items": { 36 | "type": "string" 37 | }, 38 | "type": "array", 39 | "description": "The tags of the state machine" 40 | }, 41 | "export": { 42 | "$ref": "#/$defs/Serialized", 43 | "description": "The importable version of the state machine" 44 | }, 45 | "id": { 46 | "type": "string", 47 | "description": "The ID of the state machine" 48 | }, 49 | "parent_id": { 50 | "type": "string", 51 | "description": "The ID of the parent state machine" 52 | } 53 | }, 54 | "additionalProperties": false, 55 | "type": "object", 56 | "required": [ 57 | "kind" 58 | ], 59 | "description": "GetterResp is a response to GetterReq." 60 | }, 61 | "Kind": { 62 | "properties": { 63 | "Value": { 64 | "type": "string" 65 | } 66 | }, 67 | "additionalProperties": false, 68 | "type": "object", 69 | "required": [ 70 | "Value" 71 | ] 72 | }, 73 | "S": { 74 | "items": { 75 | "type": "string" 76 | }, 77 | "type": "array" 78 | }, 79 | "Serialized": { 80 | "properties": { 81 | "id": { 82 | "type": "string" 83 | }, 84 | "time": { 85 | "$ref": "#/$defs/Time" 86 | }, 87 | "state_names": { 88 | "$ref": "#/$defs/S" 89 | } 90 | }, 91 | "additionalProperties": false, 92 | "type": "object", 93 | "required": [ 94 | "id", 95 | "time", 96 | "state_names" 97 | ] 98 | }, 99 | "Time": { 100 | "items": { 101 | "type": "integer" 102 | }, 103 | "type": "array" 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /docs/jsonschema/msg_kind_req.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://github.com/pancsta/asyncmachine-go/pkg/integrations/msg-kind-req", 4 | "$ref": "#/$defs/MsgKindReq", 5 | "$defs": { 6 | "Kind": { 7 | "properties": { 8 | "Value": { 9 | "type": "string" 10 | } 11 | }, 12 | "additionalProperties": false, 13 | "type": "object", 14 | "required": [ 15 | "Value" 16 | ] 17 | }, 18 | "MsgKindReq": { 19 | "properties": { 20 | "kind": { 21 | "$ref": "#/$defs/Kind", 22 | "description": "The kind of the request." 23 | } 24 | }, 25 | "additionalProperties": false, 26 | "type": "object", 27 | "required": [ 28 | "kind" 29 | ], 30 | "description": "MsgKindReq is a decoding helper." 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /docs/jsonschema/msg_kind_resp.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://github.com/pancsta/asyncmachine-go/pkg/integrations/msg-kind-resp", 4 | "$ref": "#/$defs/MsgKindResp", 5 | "$defs": { 6 | "Kind": { 7 | "properties": { 8 | "Value": { 9 | "type": "string" 10 | } 11 | }, 12 | "additionalProperties": false, 13 | "type": "object", 14 | "required": [ 15 | "Value" 16 | ] 17 | }, 18 | "MsgKindResp": { 19 | "properties": { 20 | "kind": { 21 | "$ref": "#/$defs/Kind", 22 | "description": "The kind of the response." 23 | } 24 | }, 25 | "additionalProperties": false, 26 | "type": "object", 27 | "required": [ 28 | "kind" 29 | ], 30 | "description": "MsgKindResp is a decoding helper." 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /docs/jsonschema/mutation_req.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://github.com/pancsta/asyncmachine-go/pkg/integrations/mutation-req", 4 | "$ref": "#/$defs/MutationReq", 5 | "$defs": { 6 | "Kind": { 7 | "properties": { 8 | "Value": { 9 | "type": "string" 10 | } 11 | }, 12 | "additionalProperties": false, 13 | "type": "object", 14 | "required": [ 15 | "Value" 16 | ] 17 | }, 18 | "MutationReq": { 19 | "oneOf": [ 20 | { 21 | "required": [ 22 | "add" 23 | ], 24 | "title": "add" 25 | }, 26 | { 27 | "required": [ 28 | "remove" 29 | ], 30 | "title": "remove" 31 | } 32 | ], 33 | "properties": { 34 | "kind": { 35 | "$ref": "#/$defs/Kind", 36 | "description": "The kind of the request." 37 | }, 38 | "add": { 39 | "$ref": "#/$defs/S", 40 | "description": "The states to add to the state machine." 41 | }, 42 | "remove": { 43 | "$ref": "#/$defs/S", 44 | "description": "The states to remove from the state machine." 45 | }, 46 | "args": { 47 | "type": "object", 48 | "description": "Arguments passed to transition handlers." 49 | } 50 | }, 51 | "additionalProperties": false, 52 | "type": "object", 53 | "required": [ 54 | "kind" 55 | ] 56 | }, 57 | "S": { 58 | "items": { 59 | "type": "string" 60 | }, 61 | "type": "array" 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /docs/jsonschema/mutation_resp.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://github.com/pancsta/asyncmachine-go/pkg/integrations/mutation-resp", 4 | "$ref": "#/$defs/MutationResp", 5 | "$defs": { 6 | "Kind": { 7 | "properties": { 8 | "Value": { 9 | "type": "string" 10 | } 11 | }, 12 | "additionalProperties": false, 13 | "type": "object", 14 | "required": [ 15 | "Value" 16 | ] 17 | }, 18 | "MutationResp": { 19 | "properties": { 20 | "kind": { 21 | "$ref": "#/$defs/Kind", 22 | "description": "The kind of the request." 23 | }, 24 | "result": { 25 | "type": "integer", 26 | "description": "The result of the mutation request." 27 | } 28 | }, 29 | "additionalProperties": false, 30 | "type": "object", 31 | "required": [ 32 | "kind", 33 | "result" 34 | ] 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /docs/jsonschema/waiting_req.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://github.com/pancsta/asyncmachine-go/pkg/integrations/waiting-req", 4 | "$ref": "#/$defs/WaitingReq", 5 | "$defs": { 6 | "Kind": { 7 | "properties": { 8 | "Value": { 9 | "type": "string" 10 | } 11 | }, 12 | "additionalProperties": false, 13 | "type": "object", 14 | "required": [ 15 | "Value" 16 | ] 17 | }, 18 | "S": { 19 | "items": { 20 | "type": "string" 21 | }, 22 | "type": "array" 23 | }, 24 | "Time": { 25 | "items": { 26 | "type": "integer" 27 | }, 28 | "type": "array" 29 | }, 30 | "WaitingReq": { 31 | "oneOf": [ 32 | { 33 | "required": [ 34 | "states" 35 | ], 36 | "title": "states" 37 | }, 38 | { 39 | "required": [ 40 | "states_not" 41 | ], 42 | "title": "statesNot" 43 | } 44 | ], 45 | "properties": { 46 | "kind": { 47 | "$ref": "#/$defs/Kind", 48 | "description": "The kind of the request." 49 | }, 50 | "states": { 51 | "$ref": "#/$defs/S", 52 | "description": "The states to wait for, the default is to all states being active simultaneously (if no time passed)." 53 | }, 54 | "states_not": { 55 | "$ref": "#/$defs/S", 56 | "description": "The states names to wait for to be inactive. Ignores the Time field." 57 | }, 58 | "time": { 59 | "$ref": "#/$defs/Time", 60 | "description": "The specific (minimal) time to wait for." 61 | } 62 | }, 63 | "additionalProperties": false, 64 | "type": "object", 65 | "required": [ 66 | "kind" 67 | ] 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /docs/jsonschema/waiting_resp.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://github.com/pancsta/asyncmachine-go/pkg/integrations/waiting-resp", 4 | "$ref": "#/$defs/WaitingResp", 5 | "$defs": { 6 | "Kind": { 7 | "properties": { 8 | "Value": { 9 | "type": "string" 10 | } 11 | }, 12 | "additionalProperties": false, 13 | "type": "object", 14 | "required": [ 15 | "Value" 16 | ] 17 | }, 18 | "S": { 19 | "items": { 20 | "type": "string" 21 | }, 22 | "type": "array" 23 | }, 24 | "Time": { 25 | "items": { 26 | "type": "integer" 27 | }, 28 | "type": "array" 29 | }, 30 | "WaitingResp": { 31 | "oneOf": [ 32 | { 33 | "required": [ 34 | "states", 35 | "states_not" 36 | ], 37 | "title": "states" 38 | } 39 | ], 40 | "properties": { 41 | "kind": { 42 | "$ref": "#/$defs/Kind", 43 | "description": "The kind of the response." 44 | }, 45 | "mach_id": { 46 | "type": "string", 47 | "description": "The ID of the state machine." 48 | }, 49 | "states": { 50 | "$ref": "#/$defs/S", 51 | "description": "The active states waited for. If time is empty, all these states are active simultaneously." 52 | }, 53 | "states_not": { 54 | "$ref": "#/$defs/S", 55 | "description": "The inactive states waited for." 56 | }, 57 | "time": { 58 | "$ref": "#/$defs/Time", 59 | "description": "The requested machine time (the current one may be higher)." 60 | } 61 | }, 62 | "additionalProperties": false, 63 | "type": "object", 64 | "required": [ 65 | "kind", 66 | "mach_id" 67 | ] 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /examples/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.go] 2 | indent_size = 4 3 | max_line_length = 120 4 | -------------------------------------------------------------------------------- /examples/arpc/Procfile: -------------------------------------------------------------------------------- 1 | client: task client 2 | server: task server 3 | -------------------------------------------------------------------------------- /examples/arpc/Taskfile.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | dotenv: [ '.env' ] 4 | 5 | tasks: 6 | client: 7 | cmd: go run ./client 8 | 9 | server: 10 | cmd: go run ./server 11 | 12 | start: 13 | desc: Start the example 14 | cmd: goreman start 15 | 16 | deps: 17 | desc: Install dependencies 18 | cmd: go install github.com/mattn/goreman@latest 19 | -------------------------------------------------------------------------------- /examples/arpc/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/rand" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | "time" 11 | 12 | "github.com/pancsta/asyncmachine-go/examples/arpc/states" 13 | amhelp "github.com/pancsta/asyncmachine-go/pkg/helpers" 14 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 15 | arpc "github.com/pancsta/asyncmachine-go/pkg/rpc" 16 | ssrpc "github.com/pancsta/asyncmachine-go/pkg/rpc/states" 17 | ) 18 | 19 | const addr = "localhost:8090" 20 | 21 | var ss = states.ExampleStates 22 | 23 | func init() { 24 | // am-dbg is required for debugging, go run it 25 | // go run github.com/pancsta/asyncmachine-go/tools/cmd/am-dbg@latest 26 | // amhelp.EnableDebugging(true) 27 | // amhelp.SetLogLevel(am.LogChanges) 28 | } 29 | 30 | func main() { 31 | ctx, cancel := context.WithCancel(context.Background()) 32 | defer cancel() 33 | 34 | // handle exit 35 | sigChan := make(chan os.Signal, 1) 36 | signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) 37 | go func() { 38 | <-sigChan 39 | cancel() 40 | }() 41 | 42 | // worker 43 | client, err := newClient(ctx, addr, states.ExampleSchema, ss.Names()) 44 | if err != nil { 45 | panic(err) 46 | } 47 | 48 | // connect 49 | client.Start() 50 | err = amhelp.WaitForAll(ctx, 3*time.Second, 51 | client.Mach.When1(ssrpc.ClientStates.Ready, ctx)) 52 | fmt.Printf("Connected to aRPC %s\n", client.Addr) 53 | 54 | // randomly mutate the remote worker 55 | t := time.NewTicker(1 * time.Second) 56 | for { 57 | exit := false 58 | select { 59 | case <-t.C: 60 | switch rand.Intn(2) { 61 | case 0: 62 | client.Worker.Add1(ss.Foo, nil) 63 | case 1: 64 | client.Worker.Add1(ss.Bar, nil) 65 | case 2: 66 | client.Worker.Add1(ss.Baz, nil) 67 | } 68 | case <-ctx.Done(): 69 | exit = true 70 | } 71 | if exit { 72 | break 73 | } 74 | } 75 | 76 | fmt.Println("bye") 77 | } 78 | 79 | func newClient( 80 | ctx context.Context, addr string, ssSchema am.Schema, ssNames am.S, 81 | ) (*arpc.Client, error) { 82 | 83 | // consumer 84 | consumer := am.New(ctx, ssrpc.ConsumerSchema, nil) 85 | err := consumer.BindHandlers(&clientHandlers{}) 86 | if err != nil { 87 | return nil, err 88 | } 89 | 90 | // init 91 | c, err := arpc.NewClient(ctx, addr, "clientid", ssSchema, ssNames, &arpc.ClientOpts{ 92 | Consumer: consumer, 93 | }) 94 | if err != nil { 95 | panic(err) 96 | } 97 | amhelp.MachDebugEnv(c.Mach) 98 | 99 | return c, nil 100 | } 101 | 102 | type clientHandlers struct { 103 | *am.ExceptionHandler 104 | } 105 | 106 | func (h *clientHandlers) WorkerPayloadState(e *am.Event) { 107 | e.Machine().Remove1(ssrpc.ConsumerStates.WorkerPayload, nil) 108 | 109 | args := arpc.ParseArgs(e.Args) 110 | println("Payload: " + args.Payload.Data.(string)) 111 | } 112 | -------------------------------------------------------------------------------- /examples/arpc/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | "time" 10 | 11 | "github.com/pancsta/asyncmachine-go/examples/arpc/states" 12 | amhelp "github.com/pancsta/asyncmachine-go/pkg/helpers" 13 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 14 | arpc "github.com/pancsta/asyncmachine-go/pkg/rpc" 15 | ssrpc "github.com/pancsta/asyncmachine-go/pkg/rpc/states" 16 | ) 17 | 18 | const addr = "localhost:8090" 19 | 20 | var ss = states.ExampleStates 21 | 22 | func init() { 23 | // am-dbg is required for debugging, go run it 24 | // go run github.com/pancsta/asyncmachine-go/tools/cmd/am-dbg@latest 25 | // amhelp.EnableDebugging(true) 26 | // amhelp.SetLogLevel(am.LogChanges) 27 | } 28 | 29 | func main() { 30 | ctx, cancel := context.WithCancel(context.Background()) 31 | defer cancel() 32 | 33 | // handle exit 34 | sigChan := make(chan os.Signal, 1) 35 | signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) 36 | go func() { 37 | <-sigChan 38 | cancel() 39 | }() 40 | 41 | // worker 42 | worker, err := newWorker(ctx) 43 | if err != nil { 44 | panic(err) 45 | } 46 | 47 | // arpc 48 | server, err := newServer(ctx, addr, worker) 49 | if err != nil { 50 | panic(err) 51 | } 52 | 53 | // server start 54 | server.Start() 55 | err = amhelp.WaitForAll(ctx, 3*time.Second, 56 | server.Mach.When1(ssrpc.ServerStates.RpcReady, ctx)) 57 | if err != nil { 58 | panic(err) 59 | } 60 | fmt.Printf("Started aRPC server on %s\n", server.Addr) 61 | 62 | // wait for a client 63 | err = amhelp.WaitForAll(ctx, 3*time.Second, 64 | server.Mach.When1(ssrpc.ServerStates.Ready, ctx)) 65 | 66 | // periodically send data to the client 67 | t := time.NewTicker(3 * time.Second) 68 | for { 69 | exit := false 70 | select { 71 | case <-t.C: 72 | worker.Add1(ssrpc.WorkerStates.SendPayload, arpc.Pass(&arpc.A{ 73 | Name: "mypayload", 74 | Payload: &arpc.ArgsPayload{ 75 | Name: "mypayload", 76 | Source: "worker1", 77 | Data: "Hello aRPC", 78 | }, 79 | })) 80 | case <-ctx.Done(): 81 | exit = true 82 | } 83 | if exit { 84 | break 85 | } 86 | } 87 | 88 | fmt.Println("bye") 89 | } 90 | 91 | func newWorker(ctx context.Context) (*am.Machine, error) { 92 | 93 | // init 94 | worker, err := am.NewCommon(ctx, "worker", states.ExampleSchema, ss.Names(), &workerHandlers{}, nil, nil) 95 | if err != nil { 96 | return nil, err 97 | } 98 | amhelp.MachDebugEnv(worker) 99 | 100 | return worker, nil 101 | } 102 | 103 | func newServer(ctx context.Context, addr string, worker *am.Machine) (*arpc.Server, error) { 104 | 105 | // init 106 | s, err := arpc.NewServer(ctx, addr, worker.Id(), worker, nil) 107 | if err != nil { 108 | panic(err) 109 | } 110 | amhelp.MachDebugEnv(s.Mach) 111 | 112 | // start 113 | s.Start() 114 | err = amhelp.WaitForAll(ctx, 2*time.Second, 115 | s.Mach.When1(ssrpc.ServerStates.RpcReady, ctx)) 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | return s, nil 121 | } 122 | 123 | type workerHandlers struct { 124 | *am.ExceptionHandler 125 | } 126 | 127 | func (h *workerHandlers) FooState(e *am.Event) { 128 | fmt.Print("FooState") 129 | } 130 | 131 | func (h *workerHandlers) BarState(e *am.Event) { 132 | fmt.Print("BarState") 133 | } 134 | 135 | func (h *workerHandlers) BazState(e *am.Event) { 136 | fmt.Print("BazState") 137 | } 138 | -------------------------------------------------------------------------------- /examples/arpc/states/ss_example.go: -------------------------------------------------------------------------------- 1 | package states 2 | 3 | import ( 4 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 5 | ssrpc "github.com/pancsta/asyncmachine-go/pkg/rpc/states" 6 | ) 7 | 8 | // ExampleStatesDef contains all the states of the Example state machine. 9 | type ExampleStatesDef struct { 10 | *am.StatesBase 11 | 12 | Foo string 13 | Bar string 14 | Baz string 15 | 16 | // inherit from rpc/WorkerStatesDef 17 | *ssrpc.WorkerStatesDef 18 | } 19 | 20 | // ExampleGroupsDef contains all the state groups Example state machine. 21 | type ExampleGroupsDef struct { 22 | Mutex S 23 | } 24 | 25 | // ExampleSchema represents all relations and properties of ExampleStates. 26 | var ExampleSchema = SchemaMerge( 27 | // inherit from rpc/WorkerSchema 28 | ssrpc.WorkerSchema, 29 | am.Schema{ 30 | 31 | ssE.Foo: {Remove: sgE.Mutex}, 32 | ssE.Bar: {Remove: sgE.Mutex}, 33 | ssE.Baz: {Remove: sgE.Mutex}, 34 | }) 35 | 36 | // EXPORTS AND GROUPS 37 | 38 | var ( 39 | ssE = am.NewStates(ExampleStatesDef{}) 40 | sgE = am.NewStateGroups(ExampleGroupsDef{ 41 | Mutex: S{ssE.Foo, ssE.Bar, ssE.Baz}, 42 | }) 43 | 44 | // ExampleStates contains all the states for the Example machine. 45 | ExampleStates = ssE 46 | // ExampleGroups contains all the state groups for the Example machine. 47 | ExampleGroups = sgE 48 | ) 49 | -------------------------------------------------------------------------------- /examples/arpc/states/states_utils.go: -------------------------------------------------------------------------------- 1 | package states 2 | 3 | import am "github.com/pancsta/asyncmachine-go/pkg/machine" 4 | 5 | // S is a type alias for a list of state names. 6 | type S = am.S 7 | 8 | // SAdd is a func alias for merging lists of states. 9 | var SAdd = am.SAdd 10 | 11 | // StateAdd is a func alias for adding to an existing state definition. 12 | var StateAdd = am.StateAdd 13 | 14 | // StateSet is a func alias for replacing parts of an existing state 15 | // definition. 16 | var StateSet = am.StateSet 17 | 18 | // SchemaMerge is a func alias for extending an existing state structure. 19 | var SchemaMerge = am.SchemaMerge 20 | -------------------------------------------------------------------------------- /examples/asynq_fileprocessing/client/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/hibiken/asynq" 7 | 8 | tasks "github.com/pancsta/asyncmachine-go/examples/asynq_fileprocessing" 9 | ) 10 | 11 | const redisAddr = "127.0.0.1:6379" 12 | 13 | func main() { 14 | client := asynq.NewClient(asynq.RedisClientOpt{Addr: redisAddr}) 15 | defer client.Close() 16 | 17 | task, err := tasks.NewFileProcessingTask("foo.txt") 18 | if err != nil { 19 | log.Fatalf("could not create task: %v", err) 20 | } 21 | info, err := client.Enqueue(task) 22 | if err != nil { 23 | log.Fatalf("could not enqueue task: %v", err) 24 | } 25 | log.Printf("enqueued task: id=%s queue=%s", info.ID, info.Queue) 26 | } 27 | -------------------------------------------------------------------------------- /examples/asynq_fileprocessing/fileprocessing_task.go: -------------------------------------------------------------------------------- 1 | // This example shows how to use AsyncMachine as an Asynq worker, which gives: 2 | // - retries / scheduling 3 | // - queue management 4 | // - network distribution 5 | // - and more, see https://github.com/hibiken/asynq 6 | // 7 | // For simplicity, we're using an existing FileProcessing example, which is a 8 | // port of the Temporal one. 9 | // See /examples/temporal-fileprocessing/fileprocessing.go 10 | // 11 | // Steps to run the example: 12 | // 1. Start redis `docker run -p 6379:6379 --rm redis` 13 | // 2. Enqueue `go run examples/asynq-fileprocessing/client/client.go` 14 | // 3. Execute `go run examples/asynq-fileprocessing/worker/worker.go` 15 | // 3. Inspect the result: 16 | // 1. `go install github.com/hibiken/asynq/tools/asynq@latest` 17 | // 2. `asynq dash` 18 | // 3. Check default->Completed 19 | // 20 | // Sample output from the worker (LogLevel == LogChanges): 21 | // 22 | // asynq: pid=1355930 2024/01/05 08:55:26.460621 INFO: Starting processing 23 | // asynq: pid=1355930 2024/01/05 08:55:26.460663 INFO: Send signal TSTP to stop processing new tasks 24 | // asynq: pid=1355930 2024/01/05 08:55:26.460671 INFO: Send signal TERM or INT to terminate the process 25 | // 2024/01/05 09:55:26 Processing file foo.txt 26 | // 2024/01/05 09:55:26 [46206] [state] +DownloadingFile 27 | // 2024/01/05 09:55:26 [46206] [external] Downloading file... foo.txt 28 | // 2024/01/05 09:55:26 [46206] [state] +FileDownloaded -DownloadingFile 29 | // 2024/01/05 09:55:26 [46206] [state:auto] +ProcessingFile 30 | // 2024/01/05 09:55:26 waiting: DownloadingFile to FileUploaded 31 | // 2024/01/05 09:55:26 [46206] [external] processFileActivity succeed /tmp/temporal_sample1517449749 32 | // 2024/01/05 09:55:26 [46206] [state] +FileProcessed -ProcessingFile 33 | // 2024/01/05 09:55:26 [46206] [external] cleanup /tmp/temporal_sample1133869176 34 | // 2024/01/05 09:55:26 [46206] [state:auto] +UploadingFile 35 | // 2024/01/05 09:55:26 [46206] [external] uploadFileActivity begin /tmp/temporal_sample1517449749 36 | // 2024/01/05 09:55:26 [46206] [external] uploadFileActivity succeed /tmp/temporal_sample1517449749 37 | // 2024/01/05 09:55:26 [46206] [state] +FileUploaded -UploadingFile 38 | // 2024/01/05 09:55:26 [46206] [external] cleanup /tmp/temporal_sample1517449749 39 | 40 | package tasks 41 | 42 | import ( 43 | "context" 44 | "encoding/json" 45 | "fmt" 46 | "log" 47 | "time" 48 | 49 | "github.com/hibiken/asynq" 50 | "github.com/joho/godotenv" 51 | 52 | processor "github.com/pancsta/asyncmachine-go/examples/temporal_fileprocessing" 53 | ) 54 | 55 | const ( 56 | TypeFileProcessing = "file:process" 57 | ) 58 | 59 | type FileProcessingPayload struct { 60 | Filename string 61 | } 62 | 63 | func init() { 64 | // load .env 65 | _ = godotenv.Load() 66 | 67 | // am-dbg is required for debugging, go run it 68 | // go run github.com/pancsta/asyncmachine-go/tools/cmd/am-dbg@latest 69 | // amhelp.EnableDebugging(false) 70 | // amhelp.SetLogLevel(am.LogChanges) 71 | } 72 | 73 | func NewFileProcessingTask(filename string) (*asynq.Task, error) { 74 | payload, err := json.Marshal(FileProcessingPayload{Filename: filename}) 75 | if err != nil { 76 | return nil, err 77 | } 78 | return asynq.NewTask(TypeFileProcessing, payload, asynq.Retention(24*time.Hour)), nil 79 | } 80 | 81 | func HandleFileProcessingTask(ctx context.Context, t *asynq.Task) error { 82 | var p FileProcessingPayload 83 | if err := json.Unmarshal(t.Payload(), &p); err != nil { 84 | return fmt.Errorf("json.Unmarshal failed: %v: %w", err, asynq.SkipRetry) 85 | } 86 | log.Printf("Processing file %s", p.Filename) 87 | machine, err := processor.FileProcessingFlow(ctx, log.Printf, p.Filename) 88 | if err != nil { 89 | return err 90 | } 91 | // save the machine state as the result 92 | ret := machine.String() 93 | if _, err := t.ResultWriter().Write([]byte(ret)); err != nil { 94 | return fmt.Errorf("failed to write task result: %v", err) 95 | } 96 | return nil 97 | } 98 | -------------------------------------------------------------------------------- /examples/asynq_fileprocessing/worker/worker.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/hibiken/asynq" 7 | 8 | tasks "github.com/pancsta/asyncmachine-go/examples/asynq_fileprocessing" 9 | ) 10 | 11 | const redisAddr = "127.0.0.1:6379" 12 | 13 | func main() { 14 | srv := asynq.NewServer( 15 | asynq.RedisClientOpt{Addr: redisAddr}, 16 | asynq.Config{ 17 | // Specify how many concurrent workers to use 18 | Concurrency: 10, 19 | // Optionally specify multiple queues with different priority. 20 | Queues: map[string]int{ 21 | "critical": 6, 22 | "default": 3, 23 | "low": 1, 24 | }, 25 | // See the godoc for other configuration options 26 | }, 27 | ) 28 | 29 | // mux maps a type to a handler 30 | mux := asynq.NewServeMux() 31 | mux.HandleFunc(tasks.TypeFileProcessing, tasks.HandleFileProcessingTask) 32 | // ...register other handlers... 33 | 34 | if err := srv.Run(mux); err != nil { 35 | log.Fatalf("could not run server: %v", err) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/benchmark_grpc/README.md: -------------------------------------------------------------------------------- 1 | # /examples/benchmark_grpc 2 | 3 | [`cd /`](/README.md) 4 | 5 | > [!NOTE] 6 | > **asyncmachine-go** is a batteries-included graph control flow library (AOP, actor model, state-machine). 7 | 8 | ## Benchmark: aRPC vs gRPC 9 | 10 | ![results - KiB transferred, number of calls](https://pancsta.github.io/assets/asyncmachine-go/arpc-vs-grpc.png) 11 | 12 | This is a simple and opinionated benchmark of a subscribe-get-process scenario, implemented in both gRPC and aRPC. 13 | Source code can be [found in /examples/benchmark_grpc](/examples/benchmark_grpc). It essentially manipulates a worker 14 | state machine via various transports. 15 | 16 | Steps: 17 | 18 | 1. **subscription**: wait for notifications 19 | 2. **getter**: get a value from the worker 20 | 3. **processing**: call an operation based on the value 21 | 22 | ### Plain Go Implementation 23 | 24 | ```go 25 | i := 0 26 | worker.Subscribe(func() { 27 | 28 | // loop 29 | i++ 30 | if i > limit { 31 | close(end) 32 | return 33 | } 34 | 35 | // value (getter) 36 | value := worker.GetValue() 37 | 38 | // call op from value (processing) 39 | switch value { 40 | case Value1: 41 | go worker.CallOp(Op1) 42 | case Value2: 43 | go worker.CallOp(Op2) 44 | case Value3: 45 | go worker.CallOp(Op3) 46 | default: 47 | // err 48 | b.Fatalf("Unknown value: %v", value) 49 | } 50 | }) 51 | 52 | worker.Start() 53 | ``` 54 | 55 | ### Results 56 | 57 | ```text 58 | $ task benchmark-grpc 59 | ... 60 | BenchmarkClientArpc 61 | client_arpc_test.go:136: Transferred: 609 bytes 62 | client_arpc_test.go:137: Calls: 4 63 | client_arpc_test.go:138: Errors: 0 64 | client_arpc_test.go:136: Transferred: 1,149,424 bytes 65 | client_arpc_test.go:137: Calls: 10,003 66 | client_arpc_test.go:138: Errors: 0 67 | BenchmarkClientArpc-8 10000 248913 ns/op 28405 B/op 766 allocs/op 68 | BenchmarkClientGrpc 69 | client_grpc_test.go:117: Transferred: 1,113 bytes 70 | client_grpc_test.go:118: Calls: 9 71 | client_grpc_test.go:119: Errors: 0 72 | client_grpc_test.go:117: Transferred: 3,400,812 bytes 73 | client_grpc_test.go:118: Calls: 30,006 74 | client_grpc_test.go:119: Errors: 0 75 | BenchmarkClientGrpc-8 10000 262693 ns/op 19593 B/op 391 allocs/op 76 | BenchmarkClientLocal 77 | BenchmarkClientLocal-8 10000 434.4 ns/op 16 B/op 1 allocs/op 78 | PASS 79 | ok github.com/pancsta/asyncmachine-go/examples/benchmark_grpc 5.187s 80 | ``` 81 | 82 | ### aRPC 83 | 84 | Worker's states can be found below. For handlers, please [refer to the source](/examples/benchmark_grpc/server_arpc.go). 85 | 86 | ```go 87 | // Machine schema defines relations and properties of states. 88 | var States = am.Schema{ 89 | // toggle 90 | Start: {}, 91 | 92 | // ops 93 | CallOp: { 94 | Multi: true, 95 | Require: S{Start}, 96 | }, 97 | 98 | // events 99 | Event: { 100 | Multi: true, 101 | Require: S{Start}, 102 | }, 103 | 104 | // values 105 | Value1: {Remove: GroupValues}, 106 | Value2: {Remove: GroupValues}, 107 | Value3: {Remove: GroupValues}, 108 | } 109 | 110 | // Groups of mutually exclusive states. 111 | 112 | var ( 113 | GroupValues = S{Value1, Value2, Value3} 114 | ) 115 | ``` 116 | 117 | ## monorepo 118 | 119 | - [`/pkg/rpc/README.md`](/pkg/rpc/README.md) 120 | - [`/examples/arpc`](/examples/arpc) 121 | - [`/pkg/rpc/HOWTO.md`](/pkg/rpc/HOWTO.md) 122 | 123 | [Go back to the monorepo root](/README.md) to continue reading. 124 | -------------------------------------------------------------------------------- /examples/benchmark_grpc/client_grpc_test.go: -------------------------------------------------------------------------------- 1 | package benchmark_grpc 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "net" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "golang.org/x/text/language" 11 | "golang.org/x/text/message" 12 | "google.golang.org/grpc" 13 | "google.golang.org/grpc/reflection" 14 | 15 | pb "github.com/pancsta/asyncmachine-go/examples/benchmark_grpc/proto" 16 | "github.com/pancsta/asyncmachine-go/internal/testing/utils" 17 | arpc "github.com/pancsta/asyncmachine-go/pkg/rpc" 18 | ) 19 | 20 | func BenchmarkClientGrpc(b *testing.B) { 21 | // init 22 | ctx := context.Background() 23 | worker := &Worker{} 24 | limit := b.N 25 | end := make(chan struct{}) 26 | calls := 0 27 | 28 | // init grpc server 29 | lis, err := net.Listen("tcp", ":50051") 30 | if err != nil { 31 | b.Fatalf("failed to listen: %v", err) 32 | } 33 | s := grpc.NewServer() 34 | service := NewWorkerServiceServer(worker) 35 | pb.RegisterWorkerServiceServer(s, service) 36 | reflection.Register(s) 37 | go s.Serve(lis) 38 | defer lis.Close() 39 | l("test", "grpc server started") 40 | serverAddr := lis.Addr().String() 41 | 42 | // monitor traffic 43 | counterListener := utils.RandListener("localhost") 44 | connAddr := counterListener.Addr().String() 45 | counter := make(chan int64, 1) 46 | go arpc.TrafficMeter(counterListener, serverAddr, counter, end) 47 | 48 | // init grpc client 49 | conn, err := grpc.NewClient(connAddr, grpc.WithInsecure()) 50 | if err != nil { 51 | b.Fatalf("did not connect: %v", err) 52 | } 53 | defer conn.Close() 54 | client := pb.NewWorkerServiceClient(conn) 55 | l("test", "grpc client started") 56 | 57 | // test subscribe-get-process 58 | // 59 | // 1. subscription: wait for notifications 60 | // 2. getter: get the value from the source 61 | // 3. processing: call an operation based on the value 62 | calls++ 63 | stream, err := client.Subscribe(ctx, &pb.Empty{}) 64 | if err != nil { 65 | b.Fatalf("Subscribe failed: %v", err) 66 | } 67 | 68 | go func() { 69 | for i := 0; i <= limit; i++ { 70 | 71 | // wait for notification (subscription) 72 | _, err := stream.Recv() 73 | if err != nil { 74 | log.Fatalf("Failed to receive a notification: %v", err) 75 | } 76 | 77 | // value (getter) 78 | calls++ 79 | respValue, err := client.GetValue(ctx, &pb.Empty{}) 80 | if err != nil { 81 | log.Fatalf("GetValue failed: %v", err) 82 | } 83 | 84 | // call op from value (processing) 85 | calls++ 86 | switch Value(respValue.Value) { 87 | case Value1: 88 | _, err = client.CallOp(ctx, &pb.CallOpRequest{Op: int32(Op1)}) 89 | case Value2: 90 | _, err = client.CallOp(ctx, &pb.CallOpRequest{Op: int32(Op2)}) 91 | case Value3: 92 | _, err = client.CallOp(ctx, &pb.CallOpRequest{Op: int32(Op3)}) 93 | default: 94 | // err 95 | b.Fatalf("Unknown value: %v", respValue.Value) 96 | } 97 | if err != nil { 98 | b.Fatalf("CallOp failed: %v", err) 99 | } 100 | } 101 | 102 | // exit 103 | close(end) 104 | }() 105 | 106 | // reset the timer to exclude setup time 107 | b.ResetTimer() 108 | 109 | // start, wait and report 110 | calls++ 111 | _, err = client.Start(ctx, &pb.Empty{}) 112 | if err != nil { 113 | log.Fatalf("Start failed: %v", err) 114 | } 115 | <-end 116 | b.ReportAllocs() 117 | p := message.NewPrinter(language.English) 118 | b.Log(p.Sprintf("Transferred: %d bytes", <-counter)) 119 | b.Log(p.Sprintf("Calls: %d", calls+service.calls)) 120 | b.Log(p.Sprintf("Errors: %d", worker.ErrCount)) 121 | b.Log(p.Sprintf("Completions: %d", worker.SuccessCount)) 122 | 123 | assert.Equal(b, 0, worker.ErrCount) 124 | assert.Greater(b, worker.SuccessCount, 0) 125 | } 126 | -------------------------------------------------------------------------------- /examples/benchmark_grpc/client_local_test.go: -------------------------------------------------------------------------------- 1 | package benchmark_grpc 2 | 3 | import "testing" 4 | 5 | func BenchmarkClientLocal(b *testing.B) { 6 | // init 7 | worker := &Worker{} 8 | i := 0 9 | limit := b.N 10 | end := make(chan struct{}) 11 | 12 | // test sub-get-process 13 | // 14 | // 1. subscription: wait for notifications 15 | // 2. getter: get a value from the worker 16 | // 3. processing: call an operation based on the value 17 | worker.Subscribe(func() { 18 | // loop 19 | i++ 20 | if i > limit { 21 | close(end) 22 | return 23 | } 24 | 25 | // value (getter) 26 | value := worker.GetValue() 27 | 28 | // call op from value (processing) 29 | switch value { 30 | case Value1: 31 | go worker.CallOp(Op1) 32 | case Value2: 33 | go worker.CallOp(Op2) 34 | case Value3: 35 | go worker.CallOp(Op3) 36 | default: 37 | // err 38 | b.Fatalf("Unknown value: %v", value) 39 | } 40 | }) 41 | 42 | // reset the timer to exclude setup time 43 | b.ResetTimer() 44 | 45 | // start, wait and report 46 | worker.Start() 47 | <-end 48 | b.ReportAllocs() 49 | } 50 | -------------------------------------------------------------------------------- /examples/benchmark_grpc/proto/worker.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package proto; 4 | option go_package = "github.com/pancsta/asyncmachine-go/examples/grpc-benchmark/proto"; 5 | 6 | service WorkerService { 7 | rpc Start (Empty) returns (Empty); 8 | rpc CallOp (CallOpRequest) returns (CallOpResponse); 9 | rpc Subscribe (Empty) returns (stream Empty); 10 | rpc GetValue (Empty) returns (GetValueResponse); 11 | } 12 | 13 | message CallOpRequest { 14 | int32 op = 1; 15 | } 16 | 17 | message CallOpResponse { 18 | bool success = 1; 19 | } 20 | 21 | message Empty { 22 | } 23 | 24 | message GetValueResponse { 25 | int32 value = 1; 26 | } -------------------------------------------------------------------------------- /examples/benchmark_grpc/server_arpc.go: -------------------------------------------------------------------------------- 1 | package benchmark_grpc 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/pancsta/asyncmachine-go/examples/benchmark_grpc/states" 8 | "github.com/pancsta/asyncmachine-go/pkg/helpers" 9 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 10 | arpc "github.com/pancsta/asyncmachine-go/pkg/rpc" 11 | ssrpc "github.com/pancsta/asyncmachine-go/pkg/rpc/states" 12 | ) 13 | 14 | var ss = states.WorkerStates 15 | 16 | type WorkerArpcServer struct { 17 | Worker *Worker 18 | Mach *am.Machine 19 | 20 | RPC *arpc.Server 21 | } 22 | 23 | func NewWorkerArpcServer( 24 | ctx context.Context, addr string, worker *Worker, 25 | ) (*WorkerArpcServer, error) { 26 | // validate 27 | if worker == nil { 28 | return nil, errors.New("worker is nil") 29 | } 30 | 31 | // init 32 | w := &WorkerArpcServer{ 33 | Worker: worker, 34 | Mach: am.New(ctx, states.WorkerSchema, &am.Opts{ID: "worker"}), 35 | } 36 | 37 | // verify states and bind to methods 38 | err := w.Mach.VerifyStates(ss.Names()) 39 | if err != nil { 40 | return nil, err 41 | } 42 | err = w.Mach.BindHandlers(w) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | // bind to worker 48 | worker.Subscribe(func() { 49 | w.Mach.Add1(ss.Event, nil) 50 | }) 51 | 52 | // server init 53 | s, err := arpc.NewServer(ctx, addr, "worker", w.Mach, nil) 54 | if err != nil { 55 | return nil, err 56 | } 57 | w.RPC = s 58 | 59 | // logging 60 | logLvl := am.EnvLogLevel("") 61 | w.RPC.Mach.SetLoggerSimple(w.log, logLvl) 62 | w.Mach.SetLoggerSimple(w.log, logLvl) 63 | 64 | // telemetry debug 65 | helpers.MachDebugEnv(w.RPC.Mach) 66 | helpers.MachDebugEnv(w.Mach) 67 | 68 | // server start 69 | w.RPC.Start() 70 | <-w.RPC.Mach.When1(ssrpc.ServerStates.RpcReady, nil) 71 | 72 | return w, nil 73 | } 74 | 75 | // methods 76 | 77 | func (w *WorkerArpcServer) log(msg string, args ...any) { 78 | l("arpc-server", msg, args...) 79 | } 80 | 81 | // handlers 82 | 83 | func (w *WorkerArpcServer) CallOpEnter(e *am.Event) bool { 84 | _, ok := e.Args["Op"].(Op) 85 | return ok 86 | } 87 | 88 | func (w *WorkerArpcServer) CallOpState(e *am.Event) { 89 | w.Mach.Remove1(ss.CallOp, nil) 90 | 91 | op := e.Args["Op"].(Op) 92 | w.Worker.CallOp(op) 93 | } 94 | 95 | func (w *WorkerArpcServer) EventState(_ *am.Event) { 96 | w.Mach.Remove1(ss.Event, nil) 97 | 98 | switch w.Worker.GetValue() { 99 | case Value1: 100 | w.Mach.Add1(ss.Value1, nil) 101 | case Value2: 102 | w.Mach.Add1(ss.Value2, nil) 103 | case Value3: 104 | w.Mach.Add1(ss.Value3, nil) 105 | } 106 | } 107 | 108 | func (w *WorkerArpcServer) StartState(_ *am.Event) { 109 | w.Worker.Start() 110 | } 111 | -------------------------------------------------------------------------------- /examples/benchmark_grpc/server_grpc.go: -------------------------------------------------------------------------------- 1 | package benchmark_grpc 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | pb "github.com/pancsta/asyncmachine-go/examples/benchmark_grpc/proto" 8 | ) 9 | 10 | type WorkerServiceServer struct { 11 | pb.UnimplementedWorkerServiceServer 12 | mu sync.Mutex 13 | worker *Worker 14 | subscriber chan struct{} 15 | ready chan struct{} 16 | calls int 17 | } 18 | 19 | func NewWorkerServiceServer(worker *Worker) *WorkerServiceServer { 20 | s := &WorkerServiceServer{ 21 | worker: worker, 22 | ready: make(chan struct{}), 23 | } 24 | 25 | worker.Subscribe(func() { 26 | s.subscriber <- struct{}{} 27 | }) 28 | 29 | return s 30 | } 31 | 32 | func (s *WorkerServiceServer) CallOp(ctx context.Context, req *pb.CallOpRequest) (*pb.CallOpResponse, error) { 33 | s.mu.Lock() 34 | defer s.mu.Unlock() 35 | 36 | op := Op(req.Op) 37 | l("grpc-server", "op: %v", op) 38 | 39 | s.worker.CallOp(op) 40 | 41 | return &pb.CallOpResponse{Success: true}, nil 42 | } 43 | 44 | func (s *WorkerServiceServer) Subscribe(req *pb.Empty, stream pb.WorkerService_SubscribeServer) error { 45 | l("grpc-server", "Subscribe") 46 | ch := make(chan struct{}, 10) 47 | s.mu.Lock() 48 | s.subscriber = ch 49 | close(s.ready) 50 | s.mu.Unlock() 51 | 52 | for range ch { 53 | l("grpc-server", "notify") 54 | s.calls++ 55 | if err := stream.Send(&pb.Empty{}); err != nil { 56 | return err 57 | } 58 | } 59 | 60 | return nil 61 | } 62 | 63 | func (s *WorkerServiceServer) GetValue(ctx context.Context, req *pb.Empty) (*pb.GetValueResponse, error) { 64 | s.mu.Lock() 65 | defer s.mu.Unlock() 66 | 67 | l("grpc-server", "GetValue") 68 | 69 | return &pb.GetValueResponse{Value: int32(s.worker.GetValue())}, nil 70 | } 71 | 72 | func (s *WorkerServiceServer) Start(ctx context.Context, req *pb.Empty) (*pb.Empty, error) { 73 | <-s.ready 74 | 75 | s.mu.Lock() 76 | defer s.mu.Unlock() 77 | 78 | l("grpc-server", "Start") 79 | s.worker.Start() 80 | 81 | return &pb.Empty{}, nil 82 | } 83 | -------------------------------------------------------------------------------- /examples/benchmark_grpc/states/ss_worker.go: -------------------------------------------------------------------------------- 1 | package states 2 | 3 | import ( 4 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 5 | ssrpc "github.com/pancsta/asyncmachine-go/pkg/rpc/states" 6 | ) 7 | 8 | // WorkerStatesDef contains all the states of the Worker state machine. 9 | type WorkerStatesDef struct { 10 | Event string 11 | Value1 string 12 | Value2 string 13 | Value3 string 14 | CallOp string 15 | 16 | // inherit from WorkerStatesDef 17 | *ssrpc.WorkerStatesDef 18 | } 19 | 20 | // WorkerGroupsDef contains all the state groups of the Worker state machine. 21 | type WorkerGroupsDef struct { 22 | 23 | // Values group contains mutually exclusive values. 24 | Values S 25 | } 26 | 27 | // WorkerSchema represents all relations and properties of WorkerStates. 28 | var WorkerSchema = SchemaMerge( 29 | // inherit from WorkerSchema 30 | ssrpc.WorkerSchema, 31 | am.Schema{ 32 | 33 | // ops 34 | ws.CallOp: { 35 | Multi: true, 36 | Require: S{ws.Start}, 37 | }, 38 | 39 | // events 40 | ws.Event: { 41 | Multi: true, 42 | Require: S{ws.Start}, 43 | }, 44 | 45 | // values 46 | ws.Value1: {Remove: wg.Values}, 47 | ws.Value2: {Remove: wg.Values}, 48 | ws.Value3: {Remove: wg.Values}, 49 | }) 50 | 51 | // EXPORTS AND GROUPS 52 | 53 | var ( 54 | // ws is worker states from WorkerStatesDef. 55 | ws = am.NewStates(WorkerStatesDef{}) 56 | 57 | // wg is worker groups from WorkerGroupsDef. 58 | wg = am.NewStateGroups(WorkerGroupsDef{ 59 | Values: S{ws.Value1, ws.Value2, ws.Value3}, 60 | }) 61 | 62 | // WorkerStates contains all the states for the Worker machine. 63 | WorkerStates = ws 64 | 65 | // WorkerGroups contains all the state groups for the Worker machine. 66 | WorkerGroups = wg 67 | ) 68 | -------------------------------------------------------------------------------- /examples/benchmark_grpc/states/states_utils.go: -------------------------------------------------------------------------------- 1 | package states 2 | 3 | import am "github.com/pancsta/asyncmachine-go/pkg/machine" 4 | 5 | // S is a type alias for a list of state names. 6 | type S = am.S 7 | 8 | // SAdd is a func alias for merging lists of states. 9 | var SAdd = am.SAdd 10 | 11 | // StateAdd is a func alias for adding to an existing state definition. 12 | var StateAdd = am.StateAdd 13 | 14 | // StateSet is a func alias for replacing parts of an existing state 15 | // definition. 16 | var StateSet = am.StateSet 17 | 18 | // SchemaMerge is a func alias for extending an existing state structure. 19 | var SchemaMerge = am.SchemaMerge 20 | -------------------------------------------------------------------------------- /examples/benchmark_grpc/worker.go: -------------------------------------------------------------------------------- 1 | package benchmark_grpc 2 | 3 | import ( 4 | "log" 5 | "math/rand" 6 | "os" 7 | ) 8 | 9 | type Op int 10 | 11 | const ( 12 | Op1 Op = iota + 1 13 | Op2 14 | Op3 15 | ) 16 | 17 | type Value int 18 | 19 | const ( 20 | Value1 Value = iota + 1 21 | Value2 22 | Value3 23 | ) 24 | 25 | type Worker struct { 26 | ErrCount int 27 | SuccessCount int 28 | value Value 29 | evListener func() 30 | } 31 | 32 | func (s *Worker) Start() { 33 | s.CallOp(Op(rand.Intn(3) + 1)) 34 | } 35 | 36 | func (s *Worker) Subscribe(lis func()) { 37 | l("worker", "Subscribe") 38 | s.evListener = lis 39 | } 40 | 41 | func (s *Worker) GetValue() Value { 42 | return s.value 43 | } 44 | 45 | func (s *Worker) CallOp(op Op) { 46 | l("worker", "Call op: %v", op) 47 | 48 | // assert the value 49 | if s.value != 0 { 50 | switch op { 51 | case Op1: 52 | if s.value != Value1 { 53 | s.ErrCount++ 54 | } 55 | s.SuccessCount++ 56 | case Op2: 57 | if s.value != Value2 { 58 | s.ErrCount++ 59 | } 60 | s.SuccessCount++ 61 | case Op3: 62 | if s.value != Value3 { 63 | s.ErrCount++ 64 | } 65 | s.SuccessCount++ 66 | default: 67 | // err 68 | s.ErrCount++ 69 | } 70 | } 71 | 72 | // create a rand value 73 | s.value = Value(rand.Intn(3) + 1) 74 | 75 | // call an event 76 | s.notify() 77 | } 78 | 79 | func (s *Worker) notify() { 80 | l("worker", "Notify") 81 | s.evListener() 82 | } 83 | 84 | func l(src, msg string, args ...any) { 85 | if os.Getenv("BENCH_DEBUG") == "" { 86 | return 87 | } 88 | log.Printf(src+": "+msg, args...) 89 | } 90 | -------------------------------------------------------------------------------- /examples/benchmark_grpc/worker_proto/worker.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package worker; 4 | option go_package = "github.com/pancsta/asyncmachine-go/examples/grpc-benchmark/worker_proto"; 5 | 6 | service WorkerService { 7 | rpc Start (Empty) returns (Empty); 8 | rpc CallOp (CallOpRequest) returns (CallOpResponse); 9 | rpc Subscribe (Empty) returns (stream Empty); 10 | rpc GetValue (Empty) returns (GetValueResponse); 11 | } 12 | 13 | message CallOpRequest { 14 | int32 op = 1; 15 | } 16 | 17 | message CallOpResponse { 18 | bool success = 1; 19 | } 20 | 21 | message Empty { 22 | } 23 | 24 | message GetValueResponse { 25 | int32 value = 1; 26 | } -------------------------------------------------------------------------------- /examples/benchmark_grpc/worker_states/worker_states.go: -------------------------------------------------------------------------------- 1 | package states 2 | 3 | import am "github.com/pancsta/asyncmachine-go/pkg/machine" 4 | 5 | // S is a type alias for a list of state names. 6 | type S = am.S 7 | 8 | // State is a type alias for a single state definition. 9 | type State = am.State 10 | 11 | // States structure defines relations and properties of states. 12 | var States = am.Schema{ 13 | // toggle 14 | Start: {}, 15 | 16 | // ops 17 | CallOp: { 18 | Multi: true, 19 | Require: S{Start}, 20 | }, 21 | 22 | // events 23 | Event: { 24 | Multi: true, 25 | Require: S{Start}, 26 | }, 27 | 28 | // values 29 | Value1: {Remove: GroupValues}, 30 | Value2: {Remove: GroupValues}, 31 | Value3: {Remove: GroupValues}, 32 | } 33 | 34 | // Groups of mutually exclusive states. 35 | 36 | var ( 37 | GroupValues = S{Value1, Value2, Value3} 38 | ) 39 | 40 | // #region boilerplate defs 41 | 42 | // Names of all the states (pkg enum). 43 | 44 | const ( 45 | Start = "Start" 46 | Event = "Event" 47 | Value1 = "Value1" 48 | Value2 = "Value2" 49 | Value3 = "Value3" 50 | CallOp = "CallOp" 51 | ) 52 | 53 | // Names is an ordered list of all the state names. 54 | var Names = S{ 55 | am.Exception, 56 | Start, 57 | Event, 58 | Value1, 59 | Value2, 60 | Value3, 61 | CallOp, 62 | } 63 | 64 | // #endregion 65 | -------------------------------------------------------------------------------- /examples/benchmark_state_source/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Golang image as the base image 2 | FROM golang:1.23-alpine AS builder 3 | 4 | # deps 5 | WORKDIR /app 6 | COPY go.mod . 7 | RUN go mod tidy 8 | 9 | # code 10 | COPY . . 11 | RUN go build -o main ./examples/tree_state_source 12 | 13 | # Start a new stage from scratch 14 | FROM alpine:latest 15 | 16 | # Set the Current Working Directory inside the container 17 | WORKDIR /root/ 18 | 19 | # Copy the Pre-built binary file from the previous stage 20 | COPY --from=builder /app/main . 21 | 22 | # Expose ports 23 | EXPOSE 19700 24 | EXPOSE 18700 25 | 26 | # Command to run the executable 27 | CMD ["./main"] 28 | -------------------------------------------------------------------------------- /examples/benchmark_state_source/Taskfile.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | tasks: 4 | 5 | start: 6 | cmds: 7 | - cd bench && ln -sfn Caddyfile-root Caddyfile 8 | - docker-compose up --detach --force-recreate --build 9 | - docker-compose stop bench 10 | 11 | stop: 12 | cmd: docker-compose down 13 | 14 | bench: 15 | cmds: 16 | - cd bench && ln -sfn Caddyfile-root Caddyfile 17 | - docker-compose restart caddy bench 18 | - sleep 25 19 | - task: logs-bench 20 | 21 | - cd bench && ln -sfn Caddyfile-root-2 Caddyfile 22 | - docker-compose restart caddy bench 23 | - sleep 25 24 | - task: logs-bench 25 | 26 | - cd bench && ln -sfn Caddyfile-root-2-4 Caddyfile 27 | - docker-compose restart caddy bench 28 | - sleep 25 29 | - task: logs-bench 30 | 31 | - cd bench && ln -sfn Caddyfile-root-2-6 Caddyfile 32 | - docker-compose restart caddy bench 33 | - sleep 25 34 | - task: logs-bench 35 | 36 | logs: 37 | interactive: true 38 | cmd: docker-compose logs --follow 39 | 40 | logs-bench: 41 | cmd: docker-compose logs --tail 22 bench 42 | -------------------------------------------------------------------------------- /examples/benchmark_state_source/bench/.gitignore: -------------------------------------------------------------------------------- 1 | Caddyfile -------------------------------------------------------------------------------- /examples/benchmark_state_source/bench/Caddyfile-root: -------------------------------------------------------------------------------- 1 | :80 { 2 | # 3 | # root only 4 | # 5 | reverse_proxy /* root:18700 6 | 7 | log { 8 | output discard 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/benchmark_state_source/bench/Caddyfile-root-2: -------------------------------------------------------------------------------- 1 | :80 { 2 | # 3 | # 2 direct replicants 4 | # 5 | reverse_proxy /* rep-1:18700 rep-2:18700 { 6 | lb_policy least_conn 7 | } 8 | 9 | log { 10 | output discard 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/benchmark_state_source/bench/Caddyfile-root-2-4: -------------------------------------------------------------------------------- 1 | :80 { 2 | # 3 | # 4 indirect replicants 4 | # 5 | reverse_proxy /* rep-1-1:18700 rep-1-2:18700 rep-2-1:18700 rep-2-2:18700 { 6 | lb_policy least_conn 7 | } 8 | 9 | log { 10 | output discard 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/benchmark_state_source/bench/Caddyfile-root-2-6: -------------------------------------------------------------------------------- 1 | :80 { 2 | # 3 | # 6 indirect replicants 4 | # 5 | reverse_proxy /* rep-1-1:18700 rep-1-2:18700 rep-1-3:18700 rep-2-1:18700 rep-2-2:18700 rep-2-3:18700 { 6 | lb_policy least_conn 7 | } 8 | 9 | log { 10 | output discard 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/benchmark_state_source/bench/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.20-alpine 2 | 3 | WORKDIR /app 4 | RUN go install github.com/tsliwowicz/go-wrk@latest 5 | 6 | CMD ["/app/go-wrk.sh"] 7 | -------------------------------------------------------------------------------- /examples/benchmark_state_source/bench/go-wrk.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | echo "----- WARMUP" 4 | echo "----- WARMUP" 5 | echo "----- WARMUP" 6 | go-wrk -c 512 -d 1 "http://caddy" 7 | sleep 3 8 | 9 | echo "----- BENCHMARK" 10 | echo "----- BENCHMARK" 11 | echo "----- BENCHMARK" 12 | echo $(date) 13 | go-wrk -c 512 -d 10 "http://caddy" 14 | -------------------------------------------------------------------------------- /examples/dag_dependency_graph/dependency_graph.go: -------------------------------------------------------------------------------- 1 | // Based on https://en.wikipedia.org/wiki/Dependency_graph 2 | // 3 | // This example shows how to construct both sync and async DAG dependency graphs using Require and Auto. 4 | // 5 | // It can be used to e.g. resolve blocking code dependencies in order. 6 | 7 | package main 8 | 9 | import ( 10 | "time" 11 | 12 | "github.com/joho/godotenv" 13 | 14 | amhelp "github.com/pancsta/asyncmachine-go/pkg/helpers" 15 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 16 | ) 17 | 18 | func init() { 19 | // load .env 20 | _ = godotenv.Load() 21 | 22 | // am-dbg is required for debugging, go run it 23 | // go run github.com/pancsta/asyncmachine-go/tools/cmd/am-dbg@latest 24 | // amhelp.EnableDebugging(false) 25 | // amhelp.SetLogLevel(am.LogChanges) 26 | } 27 | 28 | func main() { 29 | depGraph() 30 | asyncDepGraph() 31 | } 32 | 33 | // SYNC 34 | func depGraph() { 35 | // init the state machine 36 | mach := am.New(nil, am.Schema{ 37 | "A": { 38 | Auto: true, 39 | Require: am.S{"B", "C"}, 40 | }, 41 | "B": { 42 | Auto: true, 43 | Require: am.S{"D"}, 44 | }, 45 | "C": {Auto: true}, 46 | "D": {Auto: true}, 47 | "Start": {}, 48 | }, &am.Opts{LogLevel: am.LogChanges, ID: "sync"}) 49 | amhelp.MachDebugEnv(mach) 50 | _ = mach.BindHandlers(&handlers{}) 51 | mach.Add1("Start", nil) 52 | } 53 | 54 | type handlers struct{} 55 | 56 | func (h *handlers) AState(e *am.Event) { 57 | println("A ok") 58 | } 59 | 60 | func (h *handlers) BState(e *am.Event) { 61 | println("B ok") 62 | } 63 | 64 | func (h *handlers) CState(e *am.Event) { 65 | println("C ok") 66 | } 67 | 68 | func (h *handlers) DState(e *am.Event) { 69 | println("D ok") 70 | } 71 | 72 | // ASYNC 73 | 74 | func asyncDepGraph() { 75 | // init the state machine 76 | mach := am.New(nil, am.Schema{ 77 | "AInit": { 78 | Auto: true, 79 | Require: am.S{"B", "C"}, 80 | }, 81 | "A": { 82 | Require: am.S{"B", "C"}, 83 | }, 84 | "BInit": { 85 | Auto: true, 86 | Require: am.S{"D"}, 87 | }, 88 | "B": { 89 | Require: am.S{"D"}, 90 | }, 91 | "CInit": {Auto: true}, 92 | "C": {}, 93 | "DInit": {Auto: true}, 94 | "D": {}, 95 | 96 | "Start": {}, 97 | }, &am.Opts{LogLevel: am.LogChanges, ID: "async"}) 98 | amhelp.MachDebugEnv(mach) 99 | _ = mach.BindHandlers(&asyncHandlers{}) 100 | mach.Add1("Start", nil) 101 | <-mach.When1("A", nil) 102 | } 103 | 104 | type asyncHandlers struct{} 105 | 106 | func (h *asyncHandlers) AInitState(e *am.Event) { 107 | // unblock 108 | go func() { 109 | // block 110 | time.Sleep(100 * time.Millisecond) 111 | // next 112 | e.Machine().Add1("A", nil) 113 | }() 114 | } 115 | 116 | func (h *asyncHandlers) BInitState(e *am.Event) { 117 | // unblock 118 | go func() { 119 | // block 120 | time.Sleep(100 * time.Millisecond) 121 | // next 122 | e.Machine().Add1("B", nil) 123 | }() 124 | } 125 | 126 | func (h *asyncHandlers) CInitState(e *am.Event) { 127 | // unblock 128 | go func() { 129 | // block 130 | time.Sleep(100 * time.Millisecond) 131 | // next 132 | e.Machine().Add1("C", nil) 133 | }() 134 | } 135 | 136 | func (h *asyncHandlers) DInitState(e *am.Event) { 137 | // unblock 138 | go func() { 139 | // block 140 | time.Sleep(100 * time.Millisecond) 141 | // next 142 | e.Machine().Add1("D", nil) 143 | }() 144 | } 145 | 146 | func (h *asyncHandlers) AState(e *am.Event) { 147 | println("A ok") 148 | } 149 | 150 | func (h *asyncHandlers) BState(e *am.Event) { 151 | println("B ok") 152 | } 153 | 154 | func (h *asyncHandlers) CState(e *am.Event) { 155 | println("C ok") 156 | } 157 | 158 | func (h *asyncHandlers) DState(e *am.Event) { 159 | println("D ok") 160 | } 161 | -------------------------------------------------------------------------------- /examples/fan_out_in/example_fan_out_in.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/joho/godotenv" 8 | 9 | amhelp "github.com/pancsta/asyncmachine-go/pkg/helpers" 10 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 11 | ) 12 | 13 | func init() { 14 | // load .env 15 | _ = godotenv.Load() 16 | 17 | // am-dbg is required for debugging, go run it 18 | // go run github.com/pancsta/asyncmachine-go/tools/cmd/am-dbg@latest 19 | amhelp.EnableDebugging(false) 20 | amhelp.SetLogLevel(am.LogChanges) 21 | } 22 | 23 | func main() { 24 | ctx := context.Background() 25 | 26 | states := am.Schema{ 27 | // task start state 28 | "Task": {Require: am.S{"Start"}}, 29 | // task done state 30 | "TaskDone": {}, 31 | 32 | // rest 33 | 34 | "Start": {}, 35 | "Ready": { 36 | Auto: true, 37 | Require: am.S{"TaskDone"}, 38 | }, 39 | "Healthcheck": {Multi: true}, 40 | } 41 | names := am.S{"Start", "Task", "TaskDone", "Ready", "Healthcheck", am.Exception} 42 | 43 | // init state machine 44 | mach, err := am.NewCommon(ctx, "fan", states, names, &am.ExceptionHandler{}, nil, &am.Opts{ 45 | LogLevel: am.LogOps, 46 | }) 47 | if err != nil { 48 | panic(err) 49 | } 50 | amhelp.MachDebugEnv(mach) 51 | 52 | // define a task func 53 | fn := func(num int, state, stateDone string) { 54 | ctx := mach.NewStateCtx(state) 55 | go func() { 56 | if ctx.Err() != nil { 57 | return // expired 58 | } 59 | amhelp.Wait(ctx, time.Second) 60 | if ctx.Err() != nil { 61 | return // expired 62 | } 63 | mach.Add1(stateDone, nil) 64 | }() 65 | } 66 | 67 | // create task states 68 | // 10 tasks, 3 running concurrently 69 | _, err = amhelp.FanOutIn(mach, "Task", 15, 3, fn) 70 | if err != nil { 71 | panic(err) 72 | } 73 | 74 | // start and wait 75 | mach.Add(am.S{"Start", "Task"}, nil) 76 | <-mach.When1("Ready", nil) 77 | 78 | // end 79 | println("done") 80 | 81 | // debug 82 | time.Sleep(time.Second) 83 | } 84 | -------------------------------------------------------------------------------- /examples/fsm/fsm_test.go: -------------------------------------------------------------------------------- 1 | // Based on https://en.wikipedia.org/wiki/Finite-state_machine 2 | // 3 | // === RUN TestTurnstile 4 | // [state] +Locked 5 | // fsm_test.go:74: Push into (Locked:1) 6 | // fsm_test.go:79: Coin into (Locked:1) 7 | // [state] +InputCoin 8 | // [state] -InputCoin 9 | // [state] +Unlocked -Locked 10 | // fsm_test.go:85: Coin into (Unlocked:1) 11 | // [state] +InputCoin 12 | // [state] -InputCoin 13 | // fsm_test.go:91: Push into (Unlocked:1) 14 | // [state] +InputPush 15 | // [state] -InputPush 16 | // [state] +Locked -Unlocked 17 | // fsm_test.go:96: End with (Locked:3) 18 | // --- PASS: TestTurnstile (0.00s) 19 | // PASS 20 | 21 | package fsm 22 | 23 | import ( 24 | "context" 25 | "testing" 26 | "time" 27 | 28 | "github.com/stretchr/testify/assert" 29 | 30 | "github.com/joho/godotenv" 31 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 32 | ) 33 | 34 | func init() { 35 | // load .env 36 | _ = godotenv.Load() 37 | 38 | // am-dbg is required for debugging, go run it 39 | // go run github.com/pancsta/asyncmachine-go/tools/cmd/am-dbg@latest 40 | // amhelp.EnableDebugging(false) 41 | // amhelp.SetLogLevel(am.LogChanges) 42 | } 43 | 44 | // state enum pkg 45 | 46 | var ( 47 | states = am.Schema{ 48 | // input states 49 | InputPush: {}, 50 | InputCoin: {}, 51 | 52 | // "state" states 53 | Locked: { 54 | Auto: true, 55 | Remove: groupUnlocked, 56 | }, 57 | Unlocked: {Remove: groupUnlocked}, 58 | } 59 | 60 | groupUnlocked = am.S{Locked, Unlocked} 61 | 62 | InputPush = "InputPush" 63 | InputCoin = "InputCoin" 64 | Locked = "Locked" 65 | Unlocked = "Unlocked" 66 | 67 | Names = am.S{InputPush, InputCoin, Locked, Unlocked} 68 | ) 69 | 70 | // handlers 71 | 72 | type Turnstile struct{} 73 | 74 | func (t *Turnstile) InputPushEnter(e *am.Event) bool { 75 | return e.Machine().Is1(Unlocked) 76 | } 77 | 78 | func (t *Turnstile) InputPushState(e *am.Event) { 79 | e.Machine().Remove1(InputPush, nil) 80 | e.Machine().Add1(Locked, nil) 81 | } 82 | 83 | func (t *Turnstile) InputCoinState(e *am.Event) { 84 | e.Machine().Remove1(InputCoin, nil) 85 | e.Machine().Add1(Unlocked, nil) 86 | } 87 | 88 | // example 89 | 90 | func TestTurnstile(t *testing.T) { 91 | mach := am.New(context.Background(), states, &am.Opts{ 92 | ID: "turnstile", 93 | DontPanicToException: true, 94 | DontLogID: true, 95 | LogLevel: am.LogChanges, 96 | HandlerTimeout: time.Minute, 97 | }) 98 | 99 | _ = mach.BindHandlers(&Turnstile{}) 100 | _ = mach.VerifyStates(Names) 101 | 102 | // start 103 | mach.Add1(Locked, nil) 104 | assert.True(t, mach.Is1(Locked)) 105 | 106 | t.Logf("Push into %s", mach) 107 | mach.Add1(InputPush, nil) 108 | assert.True(t, mach.Is1(Locked)) 109 | 110 | // coin 111 | t.Logf("Coin into %s", mach) 112 | mach.Add1(InputCoin, nil) 113 | assert.True(t, mach.Not1(Locked)) 114 | assert.True(t, mach.Is1(Unlocked)) 115 | 116 | // coin 117 | t.Logf("Coin into %s", mach) 118 | mach.Add1(InputCoin, nil) 119 | assert.True(t, mach.Not1(Locked)) 120 | assert.True(t, mach.Is1(Unlocked)) 121 | 122 | // push 123 | t.Logf("Push into %s", mach) 124 | mach.Add1(InputPush, nil) 125 | assert.True(t, mach.Not1(Unlocked)) 126 | assert.True(t, mach.Is1(Locked)) 127 | 128 | t.Logf("End with %s", mach) 129 | } 130 | -------------------------------------------------------------------------------- /examples/mach_template/example.env: -------------------------------------------------------------------------------- 1 | # Docs available at https://asyncmachine.dev/config/env/README.md 2 | 3 | # telemetry 4 | AM_TRACE_FILTER=/home 5 | AM_HOSTNAME=dev 6 | AM_SERVICE=agent 7 | AM_PROM_PUSH_URL=http://localhost:9091 8 | AM_GRAFANA_URL=http://localhost:3000 9 | AM_GRAFANA_TOKEN= 10 | AM_OTEL_TRACE=1 11 | # AM_OTEL_TRACE_TXS=1 12 | 13 | # debug 14 | AM_DEBUG=1 15 | AM_DBG_ADDR=:6831 16 | #AM_LOG=1 17 | AM_LOG=2 18 | AM_HEALTHCHECK=1 19 | # 20 | #AM_RPC_LOG_SERVER=1 21 | #AM_RPC_LOG_CLIENT=1 22 | #AM_RPC_LOG_MUX=1 23 | #AM_RPC_DBG=1 24 | # 25 | #AM_NODE_LOG_SUPERVISOR=1 26 | #AM_NODE_LOG_CLIENT=1 27 | #AM_NODE_LOG_WORKER=1 28 | # 29 | #AM_PUBSUB_LOG=1 30 | #AM_PUBSUB_DBG=1 31 | # 32 | AM_REPL_ADDR=1 33 | AM_REPL_ADDR_DIR=tmp 34 | # go test debug 35 | # -gcflags="-N -l" -v 36 | -------------------------------------------------------------------------------- /examples/mach_template/gen-states.sh: -------------------------------------------------------------------------------- 1 | cd states 2 | am-gen states-file \ 3 | --states 'ErrExample:require(Exception),Foo:require(Bar),Bar,Baz:multi,BazDone:multi,Channel' \ 4 | --inherit basic,connected,disposed \ 5 | --name machTemplate \ 6 | --groups Group1,Group2 \ 7 | --force 8 | -------------------------------------------------------------------------------- /examples/mach_template/states/ss_mach_template.go: -------------------------------------------------------------------------------- 1 | // Package states contains a stateful schema-v2 for MachTemplate. 2 | // Bootstrapped with am-gen. Edit manually or re-gen & merge. 3 | package states 4 | 5 | import ( 6 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 7 | ss "github.com/pancsta/asyncmachine-go/pkg/states" 8 | ) 9 | 10 | // MachTemplateStatesDef contains all the states of the MachTemplate state machine. 11 | type MachTemplateStatesDef struct { 12 | *am.StatesBase 13 | 14 | ErrExample string 15 | Foo string 16 | Bar string 17 | Baz string 18 | BazDone string 19 | Channel string 20 | 21 | // inherit from BasicStatesDef 22 | *ss.BasicStatesDef 23 | // inherit from ConnectedStatesDef 24 | *ss.ConnectedStatesDef 25 | // inherit from DisposedStatesDef 26 | *ss.DisposedStatesDef 27 | } 28 | 29 | // MachTemplateGroupsDef contains all the state groups MachTemplate state machine. 30 | type MachTemplateGroupsDef struct { 31 | *ss.ConnectedGroupsDef 32 | Group1 S 33 | Group2 S 34 | } 35 | 36 | // MachTemplateSchema represents all relations and properties of MachTemplateStates. 37 | var MachTemplateSchema = SchemaMerge( 38 | // inherit from BasicSchema 39 | ss.BasicSchema, 40 | // inherit from ConnectedSchema 41 | ss.ConnectedSchema, 42 | // inherit from DisposedSchema 43 | ss.DisposedSchema, 44 | am.Schema{ 45 | 46 | ssM.ErrExample: { 47 | Require: S{ssM.Exception}, 48 | }, 49 | ssM.Foo: { 50 | Require: S{ssM.Bar}, 51 | }, 52 | ssM.Bar: {}, 53 | ssM.Baz: { 54 | Multi: true, 55 | }, 56 | ssM.BazDone: { 57 | Multi: true, 58 | }, 59 | ssM.Channel: {}, 60 | }) 61 | 62 | // EXPORTS AND GROUPS 63 | 64 | var ( 65 | ssM = am.NewStates(MachTemplateStatesDef{}) 66 | sgM = am.NewStateGroups(MachTemplateGroupsDef{ 67 | Group1: S{}, 68 | Group2: S{}, 69 | }, ss.ConnectedGroups) 70 | 71 | // MachTemplateStates contains all the states for the MachTemplate machine. 72 | MachTemplateStates = ssM 73 | // MachTemplateGroups contains all the state groups for the MachTemplate machine. 74 | MachTemplateGroups = sgM 75 | ) 76 | -------------------------------------------------------------------------------- /examples/mach_template/states/states_utils.go: -------------------------------------------------------------------------------- 1 | package states 2 | 3 | import am "github.com/pancsta/asyncmachine-go/pkg/machine" 4 | 5 | // S is a type alias for a list of state names. 6 | type S = am.S 7 | 8 | // State is a type alias for a state definition. See [am.State]. 9 | type State = am.State 10 | 11 | // SAdd is a func alias for merging lists of states. 12 | var SAdd = am.SAdd 13 | 14 | // StateAdd is a func alias for adding to an existing state definition. 15 | var StateAdd = am.StateAdd 16 | 17 | // StateSet is a func alias for replacing parts of an existing state 18 | // definition. 19 | var StateSet = am.StateSet 20 | 21 | // SchemaMerge is a func alias for extending an existing state structure. 22 | var SchemaMerge = am.SchemaMerge 23 | 24 | // Exception is a type alias for the exception state. 25 | var Exception = am.Exception 26 | -------------------------------------------------------------------------------- /examples/nfa/states/ss_nfa.go: -------------------------------------------------------------------------------- 1 | package states 2 | 3 | import ( 4 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 5 | ) 6 | 7 | // NfaStatesDef contains all the states of the Nfa state machine. 8 | type NfaStatesDef struct { 9 | *am.StatesBase 10 | 11 | Start string 12 | Ready string 13 | 14 | Input0 string 15 | Input0Done string 16 | Input1 string 17 | Input1Done string 18 | 19 | StepX string 20 | Step0 string 21 | Step1 string 22 | Step2 string 23 | Step3 string 24 | } 25 | 26 | // NfaGroupsDef contains all the state groups Nfa state machine. 27 | type NfaGroupsDef struct { 28 | Steps S 29 | } 30 | 31 | // NfaSchema represents all relations and properties of NfaStates. 32 | var NfaSchema = am.Schema{ 33 | 34 | ssN.Start: {Add: S{ssN.StepX}}, 35 | ssN.Ready: {Require: S{ssN.Start}}, 36 | 37 | ssN.Input0: { 38 | Multi: true, 39 | Require: S{ssN.Start}, 40 | }, 41 | ssN.Input0Done: { 42 | Multi: true, 43 | Require: S{ssN.Start}, 44 | }, 45 | ssN.Input1: { 46 | Multi: true, 47 | Require: S{ssN.Start}, 48 | }, 49 | ssN.Input1Done: { 50 | Multi: true, 51 | Require: S{ssN.Start}, 52 | }, 53 | 54 | ssN.StepX: {Remove: sgN.Steps}, 55 | ssN.Step0: {Remove: sgN.Steps}, 56 | ssN.Step1: {Remove: sgN.Steps}, 57 | ssN.Step2: {Remove: sgN.Steps}, 58 | ssN.Step3: {Remove: sgN.Steps}, 59 | } 60 | 61 | // EXPORTS AND GROUPS 62 | 63 | var ( 64 | ssN = am.NewStates(NfaStatesDef{}) 65 | sgN = am.NewStateGroups(NfaGroupsDef{ 66 | Steps: S{ssN.StepX, ssN.Step0, ssN.Step1, ssN.Step2, ssN.Step3}, 67 | }) 68 | 69 | // NfaStates contains all the states for the Nfa machine. 70 | NfaStates = ssN 71 | // NfaGroups contains all the state groups for the Nfa machine. 72 | NfaGroups = sgN 73 | ) 74 | -------------------------------------------------------------------------------- /examples/nfa/states/states_utils.go: -------------------------------------------------------------------------------- 1 | package states 2 | 3 | import am "github.com/pancsta/asyncmachine-go/pkg/machine" 4 | 5 | // S is a type alias for a list of state names. 6 | type S = am.S 7 | 8 | // State is a type alias for a state definition. See [am.State]. 9 | type State = am.State 10 | 11 | // SAdd is a func alias for merging lists of states. 12 | var SAdd = am.SAdd 13 | 14 | // StateAdd is a func alias for adding to an existing state definition. 15 | var StateAdd = am.StateAdd 16 | 17 | // StateSet is a func alias for replacing parts of an existing state 18 | // definition. 19 | var StateSet = am.StateSet 20 | 21 | // SchemaMerge is a func alias for extending an existing state structure. 22 | var SchemaMerge = am.SchemaMerge 23 | 24 | // Exception is a type alias for the exception state. 25 | var Exception = am.Exception 26 | -------------------------------------------------------------------------------- /examples/path_watcher/states/ss_watcher.go: -------------------------------------------------------------------------------- 1 | package states 2 | 3 | import am "github.com/pancsta/asyncmachine-go/pkg/machine" 4 | 5 | // S is a type alias for a list of state names. 6 | type S = am.S 7 | 8 | // States map defines relations and properties of states (for files). 9 | var States = am.Schema{ 10 | Init: { 11 | Add: S{Watching}, 12 | }, 13 | Watching: { 14 | Add: S{Init}, 15 | After: S{Init}, 16 | }, 17 | ChangeEvent: { 18 | Multi: true, 19 | Require: S{Watching}, 20 | }, 21 | Refreshing: { 22 | Multi: true, 23 | Remove: S{AllRefreshed}, 24 | }, 25 | Refreshed: { 26 | Multi: true, 27 | }, 28 | AllRefreshed: {}, 29 | } 30 | 31 | // StatesDir map defines relations and properties of states (for directories). 32 | var StatesDir = am.Schema{ 33 | Refreshing: { 34 | Remove: groupRefreshed, 35 | }, 36 | Refreshed: { 37 | Remove: groupRefreshed, 38 | }, 39 | DirDebounced: { 40 | Remove: groupRefreshed, 41 | }, 42 | DirCached: {}, 43 | } 44 | 45 | // Groups of mutually exclusive states. 46 | 47 | var groupRefreshed = S{Refreshing, Refreshed, DirDebounced} 48 | 49 | // #region boilerplate defs 50 | 51 | // Names of all the states (pkg enum). 52 | 53 | const ( 54 | Init = "Init" 55 | Watching = "Watching" 56 | ChangeEvent = "ChangeEvent" 57 | Refreshing = "Refreshing" 58 | Refreshed = "Refreshed" 59 | AllRefreshed = "AllRefreshed" 60 | 61 | // dir-only states 62 | 63 | DirDebounced = "DirDebounced" 64 | DirCached = "DirCached" 65 | ) 66 | 67 | // Names is an ordered list of all the state names for files. 68 | var Names = S{Init, Watching, ChangeEvent, Refreshing, Refreshed, AllRefreshed} 69 | 70 | // NamesDir is an ordered list of all the state names for directories. 71 | var NamesDir = S{Refreshing, Refreshed, DirDebounced, DirCached} 72 | 73 | // #endregion 74 | -------------------------------------------------------------------------------- /examples/pipes/example_pipes.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/joho/godotenv" 8 | 9 | amhelp "github.com/pancsta/asyncmachine-go/pkg/helpers" 10 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 11 | ampipe "github.com/pancsta/asyncmachine-go/pkg/states/pipes" 12 | ) 13 | 14 | func init() { 15 | // load .env 16 | _ = godotenv.Load() 17 | 18 | // am-dbg is required for debugging, go run it 19 | // go run github.com/pancsta/asyncmachine-go/tools/cmd/am-dbg@latest 20 | // amhelp.EnableDebugging(false) 21 | // amhelp.SetLogLevel(am.LogChanges) 22 | } 23 | 24 | func main() { 25 | ctx := context.Background() 26 | 27 | // init state machines 28 | mach1 := am.New(ctx, am.Schema{ 29 | "Ready": {}, 30 | "Foo": {}, 31 | "Bar": {}, 32 | "Custom": {}, 33 | "Healthcheck": {Multi: true}, 34 | }, &am.Opts{LogLevel: am.LogOps, ID: "source"}) 35 | mach2 := am.New(ctx, am.Schema{ 36 | "Ready": {}, 37 | "Custom": {}, 38 | "Healthcheck": {Multi: true}, 39 | }, &am.Opts{LogLevel: am.LogOps, ID: "destination"}) 40 | amhelp.MachDebugEnv(mach1) 41 | amhelp.MachDebugEnv(mach2) 42 | 43 | // pipe conventional states 44 | err := ampipe.BindReady(mach1, mach2, "", "") 45 | if err != nil { 46 | panic(err) 47 | } 48 | err = ampipe.BindErr(mach1, mach2, "") 49 | if err != nil { 50 | panic(err) 51 | } 52 | 53 | // pipe custom states (anon handlers) 54 | pipeCustom := &struct { 55 | CustomState am.HandlerFinal 56 | CustomEnd am.HandlerFinal 57 | }{ 58 | CustomState: ampipe.Add(mach1, mach2, "Custom", ""), 59 | CustomEnd: ampipe.Remove(mach1, mach2, "Custom", ""), 60 | } 61 | 62 | // bind and handle dispose 63 | if err := mach1.BindHandlers(pipeCustom); err != nil { 64 | panic(err) 65 | } 66 | 67 | // debug 68 | time.Sleep(time.Second) 69 | } 70 | -------------------------------------------------------------------------------- /examples/raw_strings/raw_strings.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/joho/godotenv" 8 | 9 | amhelp "github.com/pancsta/asyncmachine-go/pkg/helpers" 10 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 11 | ) 12 | 13 | func init() { 14 | // load .env 15 | _ = godotenv.Load() 16 | 17 | // am-dbg is required for debugging, go run it 18 | // go run github.com/pancsta/asyncmachine-go/tools/cmd/am-dbg@latest 19 | // amhelp.EnableDebugging(false) 20 | // amhelp.SetLogLevel(am.LogChanges) 21 | } 22 | 23 | func main() { 24 | // init the state machine 25 | mach := am.New(nil, am.Schema{ 26 | "ProcessingFile": { // async 27 | Remove: am.S{"FileProcessed"}, 28 | }, 29 | "FileProcessed": { // async 30 | Remove: am.S{"ProcessingFile"}, 31 | }, 32 | "InProgress": { // sync 33 | Auto: true, 34 | Require: am.S{"ProcessingFile"}, 35 | }, 36 | }, &am.Opts{LogLevel: am.LogOps, ID: "raw-strings"}) 37 | amhelp.MachDebugEnv(mach) 38 | mach.BindHandlers(&Handlers{ 39 | Filename: "README.md", 40 | }) 41 | // change the state 42 | mach.Add1("ProcessingFile", nil) 43 | 44 | // wait for completed 45 | select { 46 | case <-time.After(5 * time.Second): 47 | println("timeout") 48 | case <-mach.WhenErr(nil): 49 | println("err:", mach.Err()) 50 | case <-mach.When1("FileProcessed", nil): 51 | println("done") 52 | } 53 | } 54 | 55 | type Handlers struct { 56 | Filename string 57 | } 58 | 59 | // negotiation handler 60 | func (h *Handlers) ProcessingFileEnter(e *am.Event) bool { 61 | // read-only ops 62 | // decide if moving fwd is ok 63 | // no blocking 64 | // lock-free critical section 65 | return true 66 | } 67 | 68 | // final handler 69 | func (h *Handlers) ProcessingFileState(e *am.Event) { 70 | // read & write ops 71 | // no blocking 72 | // lock-free critical section 73 | mach := e.Machine() 74 | // tick-based context 75 | stateCtx := mach.NewStateCtx("ProcessingFile") 76 | go func() { 77 | // block in the background, locks needed 78 | if stateCtx.Err() != nil { 79 | return // expired 80 | } 81 | // blocking call 82 | err := processFile(h.Filename, stateCtx) 83 | if err != nil { 84 | mach.AddErr(err, nil) 85 | return 86 | } 87 | // re-check the tick ctx after a blocking call 88 | if stateCtx.Err() != nil { 89 | return // expired 90 | } 91 | // move to the next state in the flow 92 | mach.Add1("FileProcessed", am.A{"beaver": "1"}) 93 | }() 94 | } 95 | 96 | func processFile(name string, ctx context.Context) error { 97 | time.Sleep(1 * time.Second) 98 | return nil 99 | } 100 | -------------------------------------------------------------------------------- /examples/relations_playground/relations_playground.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import am "github.com/pancsta/asyncmachine-go/pkg/machine" 4 | 5 | const log = am.LogOps 6 | 7 | func init() { 8 | // am-dbg is required for debugging, go run it 9 | // import amhelp "github.com/pancsta/asyncmachine-go/pkg/helpers" 10 | // go run github.com/pancsta/asyncmachine-go/tools/cmd/am-dbg@latest 11 | // amhelp.EnableDebugging(false) 12 | // amhelp.SetLogLevel(am.LogChanges) 13 | } 14 | 15 | func main() { 16 | FooBar() 17 | FileProcessed() 18 | DryWaterWet() 19 | RemoveByAdd() 20 | AddOptionalRemoveMandatory() 21 | Mutex() 22 | Quiz() 23 | } 24 | 25 | func FooBar() { 26 | mach := newMach("FooBar", am.Schema{ 27 | "Foo": {Require: am.S{"Bar"}}, 28 | "Bar": {}, 29 | }) 30 | mach.Add1("Foo", nil) 31 | // TODO quiz: is Foo active? 32 | } 33 | 34 | func FileProcessed() { 35 | mach := newMach("FileProcessed", am.Schema{ 36 | "ProcessingFile": { // async 37 | Remove: am.S{"FileProcessed"}, 38 | }, 39 | "FileProcessed": { // async 40 | Remove: am.S{"ProcessingFile"}, 41 | }, 42 | "InProgress": { // sync 43 | Auto: true, 44 | Require: am.S{"ProcessingFile"}, 45 | }, 46 | }) 47 | mach.Add1("ProcessingFile", nil) 48 | // TODO quiz: is InProgress active? 49 | mach.Add1("FileProcessed", nil) 50 | } 51 | 52 | func DryWaterWet() { 53 | mach := newMach("DryWaterWet", am.Schema{ 54 | "Wet": { 55 | Require: am.S{"Water"}, 56 | }, 57 | "Dry": { 58 | Remove: am.S{"Water"}, 59 | }, 60 | "Water": { 61 | Add: am.S{"Wet"}, 62 | Remove: am.S{"Dry"}, 63 | }, 64 | }) 65 | mach.Add1("Dry", nil) 66 | mach.Add1("Water", nil) 67 | mach.Add1("Dry", nil) 68 | // TODO quiz: is Wet active? 69 | } 70 | 71 | func RemoveByAdd() { 72 | mach := newMach("RemoveByNonCalled", am.Schema{ 73 | "A": {Add: am.S{"B"}}, 74 | "B": {Remove: am.S{"C"}}, 75 | "C": {}, 76 | }) 77 | mach.Add1("C", nil) 78 | mach.Add1("A", nil) 79 | // TODO quiz: is C active? 80 | } 81 | 82 | func AddOptionalRemoveMandatory() { 83 | mach := newMach("AddIsOptional", am.Schema{ 84 | "A": {Add: am.S{"B"}}, 85 | "B": {}, 86 | "C": {Remove: am.S{"B"}}, 87 | }) 88 | mach.Add(am.S{"A", "C"}, nil) 89 | // TODO quiz: is B active? 90 | } 91 | 92 | func Mutex() { 93 | mach := newMach("Mutex", am.Schema{ 94 | "A": {Remove: am.S{"A", "B", "C"}}, 95 | "B": {Remove: am.S{"A", "B", "C"}}, 96 | "C": {Remove: am.S{"A", "B", "C"}}, 97 | }) 98 | mach.Add1("A", nil) 99 | mach.Add1("B", nil) 100 | mach.Add1("C", nil) 101 | // TODO quiz: which one is active? 102 | } 103 | 104 | func Quiz() { 105 | mach := newMach("Quiz", am.Schema{ 106 | "A": {Add: am.S{"B"}}, 107 | "B": { 108 | Require: am.S{"D"}, 109 | Add: am.S{"C"}, 110 | }, 111 | "C": {}, 112 | "D": {Remove: am.S{"C"}}, 113 | "E": {Add: am.S{"D"}}, 114 | }) 115 | mach.Add(am.S{"A", "E"}, nil) 116 | // TODO quiz: which one is active? 117 | } 118 | 119 | // playground helpers 120 | 121 | func newMach(id string, machSchema am.Schema) *am.Machine { 122 | mach := am.New(nil, machSchema, &am.Opts{ 123 | ID: id, 124 | DontLogID: true, 125 | Tracers: []am.Tracer{&Tracer{}}, 126 | LogLevel: log, 127 | }) 128 | println("\n") 129 | println("-----") 130 | println("mach: " + mach.Id()) 131 | println("-----") 132 | 133 | // DEBUG 134 | // amhelp.MachDebugEnv(mach) 135 | 136 | return mach 137 | } 138 | 139 | type Tracer struct { 140 | *am.NoOpTracer 141 | } 142 | 143 | func (t *Tracer) TransitionEnd(tx *am.Transition) { 144 | println("=> " + tx.Machine.String()) 145 | } 146 | -------------------------------------------------------------------------------- /examples/relations_playground/relations_playground_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | // simple check for panics 8 | func TestAll(t *testing.T) { 9 | FooBar() 10 | FileProcessed() 11 | DryWaterWet() 12 | RemoveByAdd() 13 | AddOptionalRemoveMandatory() 14 | Mutex() 15 | Quiz() 16 | } 17 | -------------------------------------------------------------------------------- /examples/repl/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Steps: 4 | // 1. go run . 5 | // 2. go run github.com/pancsta/asyncmachine-go/tools/cmd/arpc@latest -d tmp 6 | 7 | import ( 8 | "context" 9 | "fmt" 10 | "log" 11 | "os" 12 | "os/signal" 13 | "syscall" 14 | "time" 15 | 16 | "github.com/pancsta/asyncmachine-go/examples/arpc/states" 17 | amhelp "github.com/pancsta/asyncmachine-go/pkg/helpers" 18 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 19 | arpc "github.com/pancsta/asyncmachine-go/pkg/rpc" 20 | ) 21 | 22 | var ss = states.ExampleStates 23 | 24 | func init() { 25 | // am-dbg is required for debugging, go run it 26 | // go run github.com/pancsta/asyncmachine-go/tools/cmd/am-dbg@latest 27 | // amhelp.EnableDebugging(true) 28 | // amhelp.SetLogLevel(am.LogChanges) 29 | } 30 | 31 | func main() { 32 | ctx, cancel := context.WithCancel(context.Background()) 33 | defer cancel() 34 | 35 | // handle exit TODO not working 36 | sigChan := make(chan os.Signal, 1) 37 | signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) 38 | 39 | // workers 40 | for i := range 3 { 41 | worker, err := newWorker(ctx, i) 42 | if err != nil { 43 | log.Print(err) 44 | } 45 | 46 | time.Sleep(100 * time.Millisecond) 47 | fmt.Printf("start %d\n", i) 48 | worker.Log("hello") 49 | } 50 | 51 | // wait 52 | <-sigChan 53 | 54 | fmt.Println("bye") 55 | } 56 | 57 | func newWorker(ctx context.Context, num int) (*am.Machine, error) { 58 | // init 59 | 60 | handlers := &workerHandlers{} 61 | id := fmt.Sprintf("worker%d", num) 62 | worker, err := am.NewCommon(ctx, id, states.ExampleSchema, 63 | ss.Names(), handlers, nil, nil) 64 | if err != nil { 65 | return nil, err 66 | } 67 | handlers.Mach = worker 68 | 69 | // telemetry 70 | 71 | amhelp.MachDebugEnv(worker) 72 | // worker.SetLogArgs(am.NewArgsMapper([]string{"log"}, 0)) 73 | worker.SetLogLevel(am.LogChanges) 74 | // start a REPL aRPC server, create an addr file 75 | arpc.MachRepl(worker, "", "tmp", nil, nil) 76 | 77 | return worker, nil 78 | } 79 | 80 | type workerHandlers struct { 81 | *am.ExceptionHandler 82 | Mach *am.Machine 83 | } 84 | 85 | func (h *workerHandlers) FooState(e *am.Event) { 86 | fmt.Println("FooState") 87 | h.Mach.Log("FooState") 88 | } 89 | 90 | func (h *workerHandlers) BarState(e *am.Event) { 91 | fmt.Println("BarState") 92 | h.Mach.Log("BarState") 93 | } 94 | 95 | func (h *workerHandlers) BazState(e *am.Event) { 96 | fmt.Println("BazState") 97 | h.Mach.Log("BarState") 98 | } 99 | -------------------------------------------------------------------------------- /examples/repl/states/ss_example.go: -------------------------------------------------------------------------------- 1 | package states 2 | 3 | import ( 4 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 5 | ssrpc "github.com/pancsta/asyncmachine-go/pkg/rpc/states" 6 | ) 7 | 8 | // ExampleStatesDef contains all the states of the Example state machine. 9 | type ExampleStatesDef struct { 10 | *am.StatesBase 11 | 12 | Foo string 13 | Bar string 14 | Baz string 15 | 16 | // inherit from rpc/WorkerStatesDef 17 | *ssrpc.WorkerStatesDef 18 | } 19 | 20 | // ExampleGroupsDef contains all the state groups Example state machine. 21 | type ExampleGroupsDef struct { 22 | Mutex S 23 | } 24 | 25 | // ExampleSchema represents all relations and properties of ExampleStates. 26 | var ExampleSchema = SchemaMerge( 27 | // inherit from rpc/WorkerSchema 28 | ssrpc.WorkerSchema, 29 | am.Schema{ 30 | 31 | ssE.Foo: {Remove: sgE.Mutex}, 32 | ssE.Bar: {Remove: sgE.Mutex}, 33 | ssE.Baz: {Remove: sgE.Mutex}, 34 | }) 35 | 36 | // EXPORTS AND GROUPS 37 | 38 | var ( 39 | ssE = am.NewStates(ExampleStatesDef{}) 40 | sgE = am.NewStateGroups(ExampleGroupsDef{ 41 | Mutex: S{ssE.Foo, ssE.Bar, ssE.Baz}, 42 | }) 43 | 44 | // ExampleStates contains all the states for the Example machine. 45 | ExampleStates = ssE 46 | // ExampleGroups contains all the state groups for the Example machine. 47 | ExampleGroups = sgE 48 | ) 49 | -------------------------------------------------------------------------------- /examples/repl/states/states_utils.go: -------------------------------------------------------------------------------- 1 | package states 2 | 3 | import am "github.com/pancsta/asyncmachine-go/pkg/machine" 4 | 5 | // S is a type alias for a list of state names. 6 | type S = am.S 7 | 8 | // SAdd is a func alias for merging lists of states. 9 | var SAdd = am.SAdd 10 | 11 | // StateAdd is a func alias for adding to an existing state definition. 12 | var StateAdd = am.StateAdd 13 | 14 | // StateSet is a func alias for replacing parts of an existing state 15 | // definition. 16 | var StateSet = am.StateSet 17 | 18 | // SchemaMerge is a func alias for extending an existing state structure. 19 | var SchemaMerge = am.SchemaMerge 20 | -------------------------------------------------------------------------------- /examples/subscriptions/example_subscriptions.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "sync" 8 | "time" 9 | 10 | "github.com/joho/godotenv" 11 | 12 | amhelp "github.com/pancsta/asyncmachine-go/pkg/helpers" 13 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 14 | ) 15 | 16 | func init() { 17 | // load .env 18 | _ = godotenv.Load() 19 | 20 | // am-dbg is required for debugging, go run it 21 | // go run github.com/pancsta/asyncmachine-go/tools/cmd/am-dbg@latest 22 | // amhelp.EnableDebugging(false) 23 | // amhelp.SetLogLevel(am.LogChanges) 24 | } 25 | 26 | func main() { 27 | ctx := context.Background() 28 | 29 | // init state machines 30 | mach := am.New(ctx, am.Schema{ 31 | "Foo": {}, 32 | "Bar": {}, 33 | "Healthcheck": {Multi: true}, 34 | }, &am.Opts{LogLevel: am.LogOps, ID: "source"}) 35 | amhelp.MachDebugEnv(mach) 36 | 37 | wg := sync.WaitGroup{} 38 | 39 | wg.Add(1) 40 | go func() { 41 | go wg.Done() 42 | // wait until FileDownloaded becomes active 43 | <-mach.When1("Foo", nil) 44 | fmt.Println("1 - Foo activated") 45 | }() 46 | 47 | wg.Add(1) 48 | go func() { 49 | go wg.Done() 50 | // wait until FileDownloaded becomes inactive 51 | <-mach.WhenNot1("Bar", nil) 52 | fmt.Println("2 - Bar deactivated") 53 | }() 54 | 55 | wg.Add(1) 56 | go func() { 57 | go wg.Done() 58 | // wait for EventConnected to be activated with an arg ID=123 59 | <-mach.WhenArgs("Bar", am.A{"ID": 123}, nil) 60 | fmt.Println("3 - Bar activated with ID=123") 61 | }() 62 | 63 | wg.Add(1) 64 | go func() { 65 | go wg.Done() 66 | // wait for Foo to have a tick >= 1 and Bar tick >= 3 67 | <-mach.WhenTime(am.S{"Foo", "Bar"}, am.Time{1, 3}, nil) 68 | fmt.Println("4 - Foo tick >= 1 and Bar tick >= 3") 69 | }() 70 | 71 | wg.Add(1) 72 | go func() { 73 | go wg.Done() 74 | // wait for DownloadingFile to have a tick increased by 2 since now 75 | <-mach.WhenTicks("Foo", 2, nil) 76 | fmt.Println("5 - Foo tick increased by 2 since now") 77 | }() 78 | 79 | wg.Add(1) 80 | go func() { 81 | go wg.Done() 82 | // wait for an error 83 | <-mach.WhenErr(ctx) 84 | fmt.Println("6 - Error") 85 | }() 86 | 87 | wg.Wait() 88 | 89 | mach.Add1("Foo", nil) 90 | mach.Add1("Bar", nil) 91 | mach.Remove1("Bar", nil) 92 | mach.Remove1("Foo", nil) 93 | mach.Add1("Foo", nil) 94 | mach.Add1("Bar", am.A{"ID": 123}) 95 | mach.AddErr(errors.New("err"), nil) 96 | 97 | // wait 98 | time.Sleep(time.Second) 99 | } 100 | -------------------------------------------------------------------------------- /examples/temporal_expense/states/ss_expense.go: -------------------------------------------------------------------------------- 1 | // TODO update to v2 states file 2 | 3 | package states 4 | 5 | import am "github.com/pancsta/asyncmachine-go/pkg/machine" 6 | 7 | // S is a type alias for a list of state names. 8 | type S = am.S 9 | 10 | // States map defines relations and properties of states. 11 | var States = am.Schema{ 12 | CreatingExpense: {Remove: GroupExpense}, 13 | ExpenseCreated: {Remove: GroupExpense}, 14 | WaitingForApproval: { 15 | Auto: true, 16 | Remove: GroupApproval, 17 | }, 18 | ApprovalGranted: {Remove: GroupApproval}, 19 | PaymentInProgress: { 20 | Auto: true, 21 | Remove: GroupPayment, 22 | }, 23 | PaymentCompleted: {Remove: GroupPayment}, 24 | } 25 | 26 | // Groups of mutually exclusive states. 27 | 28 | var ( 29 | GroupExpense = S{CreatingExpense, ExpenseCreated} 30 | GroupApproval = S{WaitingForApproval, ApprovalGranted} 31 | GroupPayment = S{PaymentInProgress, PaymentCompleted} 32 | ) 33 | 34 | // #region boilerplate defs 35 | 36 | // Names of all the states (pkg enum). 37 | 38 | const ( 39 | CreatingExpense = "CreatingExpense" 40 | ExpenseCreated = "ExpenseCreated" 41 | WaitingForApproval = "WaitingForApproval" 42 | ApprovalGranted = "ApprovalGranted" 43 | PaymentInProgress = "PaymentInProgress" 44 | PaymentCompleted = "PaymentCompleted" 45 | ) 46 | 47 | // Names is an ordered list of all the state names. 48 | var Names = S{CreatingExpense, ExpenseCreated, WaitingForApproval, ApprovalGranted, PaymentInProgress, PaymentCompleted} 49 | 50 | // #endregion 51 | -------------------------------------------------------------------------------- /examples/temporal_fileprocessing/fileprocessing_test.go: -------------------------------------------------------------------------------- 1 | package temporal_fileprocessing 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | "testing" 9 | 10 | "github.com/joho/godotenv" 11 | 12 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 13 | ) 14 | 15 | func init() { 16 | // load .env 17 | _ = godotenv.Load() 18 | 19 | // am-dbg is required for debugging, go run it 20 | // go run github.com/pancsta/asyncmachine-go/tools/cmd/am-dbg@latest 21 | // amhelp.EnableDebugging(false) 22 | // amhelp.SetLogLevel(am.LogChanges) 23 | } 24 | 25 | func TestFileProcessing(t *testing.T) { 26 | ctx, cancel := context.WithCancel(context.Background()) 27 | 28 | // handle OS exit signals 29 | sigs := make(chan os.Signal, 1) 30 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 31 | go func() { 32 | <-sigs 33 | cancel() 34 | }() 35 | 36 | // start the flow and wait for the result 37 | mach, err := FileProcessingFlow(ctx, t.Logf, "foo.txt") 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | if !mach.Is(am.S{"FileUploaded"}) { 42 | t.Fatal("not FileUploaded") 43 | } 44 | 45 | t.Log(mach.String()) 46 | t.Log(mach.StringAll()) 47 | t.Log(mach.Inspect(nil)) 48 | } 49 | -------------------------------------------------------------------------------- /examples/temporal_fileprocessing/states/ss_fileprocessing.go: -------------------------------------------------------------------------------- 1 | // TODO update to v2 states file 2 | 3 | package states 4 | 5 | import am "github.com/pancsta/asyncmachine-go/pkg/machine" 6 | 7 | // S is a type alias for a list of state names. 8 | type S = am.S 9 | 10 | // States map defines relations and properties of states. 11 | var States = am.Schema{ 12 | DownloadingFile: {Remove: GroupFileDownloaded}, 13 | FileDownloaded: {Remove: GroupFileDownloaded}, 14 | ProcessingFile: { 15 | Auto: true, 16 | Require: S{FileDownloaded}, 17 | Remove: GroupFileProcessed, 18 | }, 19 | FileProcessed: {Remove: GroupFileProcessed}, 20 | UploadingFile: { 21 | Auto: true, 22 | Require: S{FileProcessed}, 23 | Remove: GroupFileUploaded, 24 | }, 25 | FileUploaded: {Remove: GroupFileUploaded}, 26 | } 27 | 28 | // Groups of mutually exclusive states. 29 | 30 | var ( 31 | GroupFileDownloaded = S{DownloadingFile, FileDownloaded} 32 | GroupFileProcessed = S{ProcessingFile, FileProcessed} 33 | GroupFileUploaded = S{UploadingFile, FileUploaded} 34 | ) 35 | 36 | // #region boilerplate defs 37 | 38 | // Names of all the states (pkg enum). 39 | 40 | const ( 41 | DownloadingFile = "DownloadingFile" 42 | FileDownloaded = "FileDownloaded" 43 | ProcessingFile = "ProcessingFile" 44 | FileProcessed = "FileProcessed" 45 | UploadingFile = "UploadingFile" 46 | FileUploaded = "FileUploaded" 47 | ) 48 | 49 | // Names is an ordered list of all the state names. 50 | var Names = S{DownloadingFile, FileDownloaded, ProcessingFile, FileProcessed, UploadingFile, FileUploaded} 51 | 52 | // #endregion 53 | -------------------------------------------------------------------------------- /examples/tree_state_source/Procfile: -------------------------------------------------------------------------------- 1 | root: task root 2 | 3 | rep-1: task rep-1 4 | rep-2: task rep-2 5 | 6 | rep-1-1: task rep-1-1 7 | rep-1-2: task rep-1-2 8 | rep-2-1: task rep-2-1 9 | rep-2-2: task rep-2-2 10 | -------------------------------------------------------------------------------- /examples/tree_state_source/README.md: -------------------------------------------------------------------------------- 1 | # /example/tree_state_source 2 | 3 | [`cd /`](/README.md) 4 | 5 | > [!NOTE] 6 | > **asyncmachine-go** is a batteries-included graph control flow library (AOP, actor model, state-machine). 7 | 8 | State source of flight statuses - in the real world this data-oriented problem should be modelled using composition and 9 | handler delegation, but it's flat in this example for simplicity and research purposes. 10 | 11 | ![diagram](https://github.com/pancsta/assets/blob/main/asyncmachine-go/diagrams/diagram_ex_1.svg) 12 | 13 | ## States 14 | 15 | The repo version has 5 flights and 3 gates (149 states). The amount of flights and gates can be adjusted and 16 | re-generated using `go run ./gen_states`. Full states file is available at [`./states/ss_flights.go`](./states/ss_flights.go). 17 | Sample definition without relations can be found below. 18 | 19 | ```go 20 | // FlightsStatesDef contains all the states of the Flights state machine. 21 | type FlightsStatesDef struct { 22 | *am.StatesBase 23 | 24 | Flight1OnTime string 25 | Flight1Delayed string 26 | Flight1Departed string 27 | Flight1Arrived string 28 | Flight1Scheduled string 29 | Flight1Inbound string 30 | Flight1Outbound string 31 | Flight1GoToGate string 32 | Flight1GateUnknown string 33 | Flight1Gate1 string 34 | Flight1Gate2 string 35 | Flight1Gate3 string 36 | Flight1Gate4 string 37 | Flight1Gate5 string 38 | 39 | // ... 40 | 41 | // inherit from rpc/WorkerStatesDef 42 | *ssrpc.WorkerStatesDef 43 | } 44 | ``` 45 | 46 | ## Actors 47 | 48 | - [root](./state_root/state_root.go) 49 | - [replicant L1](./state_root/state_root.go) 50 | - [replicant L2](./state_root/state_root.go) 51 | 52 | Scenario: 53 | 54 | - root process runs the flight-status state machine 55 | - replicants of the 1st level connect to the root 56 | - replicants of the 2st level connect to replicants of the 1st level 57 | - state clocks flow from the root to replicants 58 | - http clients GET active states from the 2nd level replicants 59 | - all state machines are connected to am-dbg, prometheus, and loki 60 | 61 | ## Running 62 | 63 | From the monorepo root: 64 | 65 | 1. `task web-metrics` 66 | 2. `task gen-grafana-tree-state-source` 67 | 3. `task tree-state-source` 68 | 4. [http://localhost:3000](http://localhost:3000) -> Dashboards -> tree-state-source 69 | 70 | ## Possibilities 71 | 72 | - L1 and L2 replicants can execute handlers locally (TODO) 73 | - root-L1 can have shorter push interval than L1-L2 74 | 75 | ## Benchmark 76 | 77 | HTTP endpoints have been benchmarked using [go-wrk](https://github.com/tsliwowicz/go-wrk), see [/examples/benchmark_state_source](/examples/benchmark_state_source/README.md) 78 | for more info. 79 | 80 | ## Config 81 | 82 | ```bash 83 | # grafana sync 84 | 85 | GRAFANA_TOKEN= 86 | GRAFANA_URL=http://localhost:3000 87 | 88 | # metrics 89 | 90 | PUSH_GATEWAY_URL=http://localhost:9091 91 | LOKI_ADDR=localhost:3100 92 | AM_LOG=2 93 | 94 | # AM debugging 95 | 96 | #AM_DEBUG=1 97 | #AM_DBG_ADDR=:6831 98 | #AM_HEALTHCHECK=1 99 | 100 | #AM_RPC_LOG_SERVER=1 101 | #AM_RPC_LOG_CLIENT=1 102 | #AM_RPC_LOG_MUX=1 103 | 104 | #AM_NODE_LOG_SUPERVISOR=1 105 | #AM_NODE_LOG_CLIENT=1 106 | #AM_NODE_LOG_WORKER=1 107 | ``` 108 | 109 | ## monorepo 110 | 111 | [Go back to the monorepo root](/README.md) to continue reading. 112 | -------------------------------------------------------------------------------- /examples/tree_state_source/Taskfile.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | dotenv: [ '.env' ] 4 | 5 | tasks: 6 | root: 7 | env: 8 | TST_ADDR: :19700 9 | TST_HTTP_ADDR: :18700 10 | cmd: go run ./ 11 | 12 | # :19700 13 | rep-1: 14 | env: 15 | TST_PARENT_ADDR: :19700 16 | TST_NAME: rep-1 17 | TST_ADDR: :19701 18 | TST_HTTP_ADDR: :18701 19 | cmd: go run ./ 20 | 21 | rep-2: 22 | env: 23 | TST_PARENT_ADDR: :19700 24 | TST_NAME: rep-2 25 | TST_ADDR: :19702 26 | TST_HTTP_ADDR: :18702 27 | cmd: go run ./ 28 | 29 | # :19701 30 | rep-1-1: 31 | env: 32 | TST_PARENT_ADDR: :19701 33 | TST_NAME: rep-1-1 34 | TST_ADDR: :19703 35 | TST_HTTP_ADDR: :18703 36 | cmd: go run ./ 37 | 38 | rep-1-2: 39 | env: 40 | TST_PARENT_ADDR: :19701 41 | TST_NAME: rep-1-2 42 | TST_ADDR: :19704 43 | TST_HTTP_ADDR: :18704 44 | cmd: go run ./ 45 | 46 | # :19702 47 | rep-2-1: 48 | env: 49 | TST_PARENT_ADDR: :19702 50 | TST_NAME: rep-2-1 51 | TST_ADDR: :19705 52 | TST_HTTP_ADDR: :18705 53 | cmd: go run ./ 54 | 55 | rep-2-2: 56 | env: 57 | TST_PARENT_ADDR: :19702 58 | TST_NAME: rep-2-2 59 | TST_ADDR: :19706 60 | TST_HTTP_ADDR: :18706 61 | cmd: go run ./ 62 | 63 | start: 64 | desc: Start the example 65 | cmd: goreman start 66 | 67 | web-metrics: 68 | dir: ../.. 69 | cmd: task web-metrics 70 | 71 | gen-grafanas: 72 | cmds: 73 | - task: gen-grafana-root 74 | - task: gen-grafana-rep 75 | vars: 76 | NAME: rep_1 77 | PARENT: root 78 | - task: gen-grafana-rep 79 | vars: 80 | NAME: rep_2 81 | PARENT: root 82 | - task: gen-grafana-rep 83 | vars: 84 | NAME: rep_1_1 85 | PARENT: rep_1_root 86 | - task: gen-grafana-rep 87 | vars: 88 | NAME: rep_1_2 89 | PARENT: rep_1_root 90 | - task: gen-grafana-rep 91 | vars: 92 | NAME: rep_2_1 93 | PARENT: rep_2_root 94 | - task: gen-grafana-rep 95 | vars: 96 | NAME: rep_2_2 97 | PARENT: rep_2_root 98 | 99 | gen-grafana-root: 100 | internal: true 101 | cmd: go run ../../tools/cmd/am-gen grafana 102 | --name root 103 | --folder tree_state_source 104 | --ids root,rm-root,rs-root-0,rs-root-1,rs-root-2 105 | --grafana-url {{.GRAFANA_URL}} 106 | --source tree_state_source_root 107 | 108 | gen-grafana-rep: 109 | internal: true 110 | cmd: go run ../../tools/cmd/am-gen grafana 111 | --name {{.NAME}} 112 | --folder tree_state_source 113 | --ids {{.NAME}}_{{.PARENT}},rc_{{.NAME}},rm_{{.NAME}},rs_{{.NAME}}_0,rs_{{.NAME}}_1,rs_{{.NAME}}_2 114 | --grafana-url {{.GRAFANA_URL}} 115 | --source tree_state_source_{{.NAME}} -------------------------------------------------------------------------------- /examples/tree_state_source/config.env: -------------------------------------------------------------------------------- 1 | # copy to .env and paste the token 2 | 3 | GRAFANA_URL=http://localhost:3000 4 | GRAFANA_TOKEN= 5 | 6 | # metrics 7 | 8 | PUSH_GATEWAY_URL=http://localhost:9091 9 | LOKI_ADDR=localhost:3100 10 | AM_LOG=2 11 | 12 | #AM_DEBUG=1 13 | #AM_DBG_ADDR=:6831 14 | #AM_HEALTHCHECK=1 15 | # 16 | #AM_RPC_LOG_SERVER=1 17 | #AM_RPC_LOG_CLIENT=1 18 | #AM_RPC_LOG_MUX=1 19 | # 20 | #AM_NODE_LOG_SUPERVISOR=1 21 | #AM_NODE_LOG_CLIENT=1 22 | #AM_NODE_LOG_WORKER=1 23 | # 24 | -------------------------------------------------------------------------------- /examples/tree_state_source/gen-grafana.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # root 4 | go ../../tools/cmd/am-gen grafana 5 | --name root 6 | --folder tree_state_source 7 | --ids root,_rm-root,_rs-root-0,_rs-root-1,_rs-root-2 8 | --grafana-url http://localhost:3000 9 | --source tree_state_source_root 10 | 11 | # replicant 1 12 | go ../../tools/cmd/am-gen grafana 13 | --name rep-1 14 | --folder tree_state_source 15 | --grafana-url http://localhost:3000 16 | --source tree_state_source_rep_1 17 | 18 | # replicant 2 19 | go ../../tools/cmd/am-gen grafana 20 | --name rep-2 21 | --folder tree_state_source 22 | --grafana-url http://localhost:3000 23 | --source tree_state_source_rep_2 24 | 25 | # replicant 1 of replicant 1 26 | go ../../tools/cmd/am-gen grafana 27 | --name rep-1-1 28 | --folder tree_state_source 29 | --grafana-url http://localhost:3000 30 | --source tree_state_source_rep_1_1 31 | 32 | # replicant 2 of replicant 1 33 | go ../../tools/cmd/am-gen grafana 34 | --name rep-1-2 35 | --folder tree_state_source 36 | --grafana-url http://localhost:3000 37 | --source tree_state_source_rep_1_2 38 | 39 | # replicant 1 of replicant 2 40 | go ../../tools/cmd/am-gen grafana 41 | --name rep-2-1 42 | --folder tree_state_source 43 | --grafana-url http://localhost:3000 44 | --source tree_state_source_rep_2_1 45 | 46 | # replicant 2 of replicant 2 47 | go ../../tools/cmd/am-gen grafana 48 | --name rep-2-2 49 | --folder tree_state_source 50 | --grafana-url http://localhost:3000 51 | --source tree_state_source_rep_2_2 52 | -------------------------------------------------------------------------------- /examples/tree_state_source/gen_states/gen_states.go: -------------------------------------------------------------------------------- 1 | /* 2 | # Schema 3 | 4 | Flight1GoToGate 5 | 6 | Status: 7 | Flight1OnTime 8 | Flight1Delayed 9 | Flight1Departed 10 | Flight1Arrived 11 | Flight1Scheduled 12 | 13 | Direction: 14 | Flight1Inbound 15 | Flight1Outbound 16 | 17 | Gates: 18 | Flight1GateUnknown 19 | Flight1Gate1 20 | Flight1Gate2 21 | Flight1Gate3 22 | */ 23 | 24 | package main 25 | 26 | import ( 27 | "context" 28 | "os" 29 | "strconv" 30 | "strings" 31 | 32 | "github.com/pancsta/asyncmachine-go/tools/generator" 33 | "github.com/pancsta/asyncmachine-go/tools/generator/cli" 34 | ) 35 | 36 | const ( 37 | flights = 10 38 | gates = 5 39 | ) 40 | 41 | func main() { 42 | ctx := context.Background() 43 | 44 | // TODO am.Schema to cli.SFParams converter 45 | params := cli.SFParams{ 46 | Name: "Flights", 47 | Inherit: "rpc/worker", 48 | } 49 | 50 | for i := 1; i <= flights; i++ { 51 | numF := strconv.Itoa(i) 52 | flight := "Flight" + numF 53 | params.States += "," 54 | params.Groups += "," 55 | 56 | // Status 57 | params.States += flight + "OnTime:remove(_" + flight + "Status)," + 58 | flight + "Delayed:remove(_" + flight + "Status)," + 59 | flight + "Departed:remove(_" + flight + "Status;" + flight + "GoToGate)," + 60 | flight + "Arrived:remove(_" + flight + "Status)," + 61 | flight + "Scheduled:auto:remove(_" + flight + "Status)," + 62 | // Direction 63 | flight + "Inbound:remove(_" + flight + "Direction)," + 64 | flight + "Outbound:remove(_" + flight + "Direction)," + 65 | // Gates 66 | flight + "GoToGate:require(" + flight + "Outbound)," + 67 | flight + "GateUnknown:auto," 68 | 69 | // Direction 70 | params.Groups += flight + "Direction(" + 71 | flight + "Inbound;" + flight + "Outbound)," 72 | 73 | // Status 74 | params.Groups += flight + "Status(" + 75 | flight + "OnTime;" + flight + "Delayed;" + flight + "Departed;" + flight + "Arrived;" + flight + "Scheduled)," 76 | 77 | // Gates 78 | params.Groups += flight + "Gates(" 79 | for ii := 1; ii <= gates; ii++ { 80 | numG := strconv.Itoa(ii) 81 | gate := flight + "Gate" + numG 82 | 83 | params.States += gate + ":remove(_" + flight + "Gates)," 84 | params.Groups += gate + ";" 85 | } 86 | params.Groups = strings.TrimRight(params.Groups, ";") + ")," 87 | } 88 | 89 | gen, err := generator.NewSFGenerator(ctx, params) 90 | if err != nil { 91 | panic(err) 92 | } 93 | 94 | // save to ../states/ss_random_data.go 95 | out := gen.Output() 96 | err = os.WriteFile("../states/ss_flights.go", []byte(out), 0644) 97 | if err != nil { 98 | panic(err) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /examples/tree_state_source/states/states_utils.go: -------------------------------------------------------------------------------- 1 | package states 2 | 3 | import am "github.com/pancsta/asyncmachine-go/pkg/machine" 4 | 5 | // S is a type alias for a list of state names. See [am.S]. 6 | type S = am.S 7 | 8 | // State is a type alias for a state definition. See [am.State]. 9 | type State = am.State 10 | 11 | // SAdd is a func alias for merging lists of states. See [am.SAdd]. 12 | var SAdd = am.SAdd 13 | 14 | // StateAdd is a func alias for adding to an existing state definition. See [am.StateAdd]. 15 | var StateAdd = am.StateAdd 16 | 17 | // StateSet is a func alias for replacing parts of an existing state 18 | // definition. See [am.StateSet]. 19 | var StateSet = am.StateSet 20 | 21 | // SchemaMerge is a func alias for extending an existing state structure. See [am.SchemaMerge]. 22 | var SchemaMerge = am.SchemaMerge 23 | -------------------------------------------------------------------------------- /internal/testing/cmd/am-dbg-worker/main_dbg_worker.go: -------------------------------------------------------------------------------- 1 | // AM_DBG_WORKER_ADDR 2 | // AM_DBG_ADDR 3 | package main 4 | 5 | import ( 6 | "context" 7 | "flag" 8 | "os" 9 | "time" 10 | 11 | amtest "github.com/pancsta/asyncmachine-go/internal/testing" 12 | "github.com/pancsta/asyncmachine-go/pkg/helpers" 13 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 14 | "github.com/pancsta/asyncmachine-go/pkg/rpc" 15 | ssrpc "github.com/pancsta/asyncmachine-go/pkg/rpc/states" 16 | "github.com/pancsta/asyncmachine-go/pkg/telemetry" 17 | "github.com/pancsta/asyncmachine-go/tools/debugger" 18 | "github.com/pancsta/asyncmachine-go/tools/debugger/server" 19 | ssdbg "github.com/pancsta/asyncmachine-go/tools/debugger/states" 20 | ) 21 | 22 | func main() { 23 | ctx, cancel := context.WithCancel(context.Background()) 24 | defer cancel() 25 | 26 | // read env 27 | amDbgAddr := os.Getenv(telemetry.EnvAmDbgAddr) 28 | logLvl := am.EnvLogLevel("") 29 | 30 | // test worker has its own flags 31 | serverAddr := flag.String("server-addr", "", 32 | "Addr of the debugger server (opt)") 33 | workerAddr := flag.String("worker-addr", amtest.WorkerRpcAddr, 34 | "Addr of the rpc worker") 35 | flag.Parse() 36 | 37 | // worker init 38 | os.Setenv(am.EnvAmLogFile, "1") 39 | dbg, err := amtest.NewDbgWorker(true, debugger.Opts{ 40 | ServerAddr: *serverAddr, 41 | }) 42 | if err != nil { 43 | panic(err) 44 | } 45 | 46 | // server init 47 | s, err := rpc.NewServer(ctx, *workerAddr, "worker", dbg.Mach, nil) 48 | if err != nil { 49 | panic(err) 50 | } 51 | helpers.MachDebug(s.Mach, amDbgAddr, logLvl, false) 52 | 53 | // tear down 54 | defer func() { 55 | if amDbgAddr != "" { 56 | // cool off am-dbg and free the ports 57 | time.Sleep(100 * time.Millisecond) 58 | } 59 | }() 60 | 61 | // am-dbg server (used for testing live connections) 62 | if *serverAddr != "" { 63 | go server.StartRpc(dbg.Mach, *serverAddr, nil, nil) 64 | } 65 | 66 | // start with a timeout 67 | readyCtx, cancel := context.WithTimeout(ctx, 3*time.Second) 68 | defer cancel() 69 | 70 | // server start 71 | s.Start() 72 | select { 73 | case <-s.Mach.WhenErr(readyCtx): 74 | err := s.Mach.Err() 75 | if readyCtx.Err() != nil { 76 | err = readyCtx.Err() 77 | } 78 | panic(err) 79 | case <-s.Mach.When1(ssrpc.ServerStates.RpcReady, readyCtx): 80 | } 81 | 82 | // wait till the end 83 | select { 84 | case <-dbg.Mach.WhenDisposed(): 85 | // user exit 86 | case <-dbg.Mach.WhenNot1(ssdbg.Start, nil): 87 | dbg.Dispose() 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /internal/testing/states/ss_rel.go: -------------------------------------------------------------------------------- 1 | // TODO rewrite to v2 2 | 3 | package states 4 | 5 | import ( 6 | "context" 7 | 8 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 9 | ) 10 | 11 | // S is a type alias for a list of state names. 12 | type S = am.S 13 | 14 | // States map defines relations and properties of states. 15 | // TODO rename to rel 16 | var States = am.Schema{ 17 | A: { 18 | Auto: true, 19 | Require: S{C}, 20 | }, 21 | B: { 22 | Multi: true, 23 | Add: S{C}, 24 | }, 25 | C: { 26 | After: S{D}, 27 | }, 28 | D: { 29 | Add: S{C, B}, 30 | }, 31 | } 32 | 33 | // Groups of mutually exclusive states. 34 | 35 | // var ( 36 | // GroupPlaying = S{Playing, Paused} 37 | // ) 38 | 39 | // #region boilerplate defs 40 | 41 | // Names of all the states (pkg enum). 42 | 43 | const ( 44 | A = "A" 45 | B = "B" 46 | C = "C" 47 | D = "D" 48 | ) 49 | 50 | // Names is an ordered list of all the state names. 51 | var Names = S{ 52 | am.Exception, 53 | A, 54 | B, 55 | C, 56 | D, 57 | } 58 | 59 | // #endregion 60 | 61 | func NewRel(ctx context.Context) *am.Machine { 62 | return am.New(ctx, States, nil) 63 | } 64 | -------------------------------------------------------------------------------- /internal/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/hex" 6 | "fmt" 7 | "os" 8 | "runtime/debug" 9 | "slices" 10 | "strings" 11 | 12 | "github.com/lithammer/dedent" 13 | ) 14 | 15 | // EnvAmHostname will override the hostname in all machine names. 16 | const EnvAmHostname = "AM_HOSTNAME" 17 | 18 | // J joins state names into a single string 19 | func J(states []string) string { 20 | return strings.Join(states, " ") 21 | } 22 | 23 | // Jw joins state names into a single string with a separator. 24 | func Jw(states []string, sep string) string { 25 | return strings.Join(states, sep) 26 | } 27 | 28 | func GetVersion() string { 29 | build, ok := debug.ReadBuildInfo() 30 | if !ok { 31 | return "(devel)" 32 | } 33 | 34 | ver := build.Main.Version 35 | if ver == "" { 36 | return "(devel)" 37 | } 38 | 39 | return ver 40 | } 41 | 42 | // TODO remove if that speeds things up 43 | func CloseSafe[T any](ch chan T) { 44 | select { 45 | case <-ch: 46 | default: 47 | close(ch) 48 | } 49 | } 50 | 51 | func SlicesWithout[S ~[]E, E comparable](coll S, el E) S { 52 | idx := slices.Index(coll, el) 53 | ret := slices.Clone(coll) 54 | if idx == -1 { 55 | return ret 56 | } 57 | return slices.Delete(ret, idx, idx+1) 58 | } 59 | 60 | // SlicesNone returns true if none of the elements of coll2 are in coll1. 61 | func SlicesNone[S1 ~[]E, S2 ~[]E, E comparable](col1 S1, col2 S2) bool { 62 | for _, el := range col2 { 63 | if slices.Contains(col1, el) { 64 | return false 65 | } 66 | } 67 | return true 68 | } 69 | 70 | // SlicesEvery returns true if all elements of coll2 are in coll1. 71 | func SlicesEvery[S1 ~[]E, S2 ~[]E, E comparable](col1 S1, col2 S2) bool { 72 | for _, el := range col2 { 73 | if !slices.Contains(col1, el) { 74 | return false 75 | } 76 | } 77 | return true 78 | } 79 | 80 | func SlicesUniq[S ~[]E, E comparable](coll S) S { 81 | var ret S 82 | for _, el := range coll { 83 | if !slices.Contains(ret, el) { 84 | ret = append(ret, el) 85 | } 86 | } 87 | return ret 88 | } 89 | 90 | // RandId generates a random ID of the given length (defaults to 8). 91 | func RandId(strLen int) string { 92 | if strLen == 0 { 93 | strLen = 16 94 | } 95 | strLen = strLen / 2 96 | 97 | id := make([]byte, strLen) 98 | _, err := rand.Read(id) 99 | if err != nil { 100 | return "error" 101 | } 102 | 103 | return hex.EncodeToString(id) 104 | } 105 | 106 | func Hostname() string { 107 | if h := os.Getenv(EnvAmHostname); h != "" { 108 | return h 109 | } 110 | host, _ := os.Hostname() 111 | if host == "" { 112 | host = "localhost" 113 | } 114 | 115 | return host 116 | } 117 | 118 | func Sp(txt string, args ...any) string { 119 | return fmt.Sprintf(dedent.Dedent(strings.Trim(txt, "\n")), args...) 120 | } 121 | 122 | func P(txt string, args ...any) { 123 | fmt.Printf(dedent.Dedent(strings.Trim(txt, "\n")), args...) 124 | } 125 | -------------------------------------------------------------------------------- /pkg/helpers/help_test.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | type A struct { 10 | Local struct{} 11 | Log string `log:"log1"` 12 | Plain string 13 | } 14 | 15 | type ARpc struct { 16 | Log string `log:"log1"` 17 | Plain string 18 | } 19 | 20 | func TestArgsRpc(t *testing.T) { 21 | a := &A{ 22 | Local: struct{}{}, 23 | Log: "log", 24 | Plain: "plain", 25 | } 26 | expected := &ARpc{ 27 | Log: "log", 28 | Plain: "plain", 29 | } 30 | 31 | rpc := ArgsToArgs(a, &ARpc{}) 32 | 33 | assert.IsType(t, &ARpc{}, rpc) 34 | assert.Equal(t, expected.Log, rpc.Log) 35 | assert.Equal(t, expected.Plain, rpc.Plain) 36 | } 37 | 38 | func TestArgsToLogMap(t *testing.T) { 39 | a := &A{ 40 | Local: struct{}{}, 41 | Log: "log", 42 | Plain: "plain", 43 | } 44 | 45 | expected := map[string]string{ 46 | "log1": "log", 47 | } 48 | 49 | logMap := ArgsToLogMap(a) 50 | 51 | assert.Equal(t, expected["log1"], logMap["log1"]) 52 | assert.Len(t, logMap, 1) 53 | } 54 | -------------------------------------------------------------------------------- /pkg/history/README.md: -------------------------------------------------------------------------------- 1 | # /pkg/history 2 | 3 | [`cd /`](/README.md) 4 | 5 | > [!NOTE] 6 | > **asyncmachine-go** is a batteries-included graph control flow library (AOP, actor model, state-machine). 7 | 8 | **/pkg/history** provides mutation history tracking and traversal. It's in an early stage, but it has a very important 9 | role in making informed decision about state flow. Besides providing a log of changes, it also binds human time to 10 | [machine time](/docs/manual.md#clock-and-context). 11 | 12 | - [`History.ActivatedRecently(state, duration)`](https://pkg.go.dev/github.com/pancsta/asyncmachine-go/pkg/history#History.ActivatedRecently) 13 | 14 | ### Installation 15 | 16 | ```go 17 | import amhist "github.com/pancsta/asyncmachine-go/pkg/states/history" 18 | ``` 19 | 20 | ## Usage 21 | 22 | ```go 23 | // create a history 24 | hist := Track(mach, am.S{"A", "C"}, 0) 25 | 26 | // mutate 27 | mach.Add1("A", nil) 28 | 29 | // run a query 30 | hist.ActivatedRecently("A", time.Second) // true 31 | ``` 32 | 33 | ## TODO 34 | 35 | - MatchEntries 36 | - StatesActiveDuring 37 | - StatesInactiveDuring 38 | - MaxLimits 39 | 40 | ## Documentation 41 | 42 | - [godoc /pkg/history](https://pkg.go.dev/github.com/pancsta/asyncmachine-go/pkg/history) 43 | 44 | ## Status 45 | 46 | Testing, not semantically versioned. 47 | 48 | ## monorepo 49 | 50 | [Go back to the monorepo root](/README.md) to continue reading. 51 | -------------------------------------------------------------------------------- /pkg/history/history_test.go: -------------------------------------------------------------------------------- 1 | package history 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | 11 | amhelp "github.com/pancsta/asyncmachine-go/pkg/helpers" 12 | 13 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 14 | ) 15 | 16 | func ExampleTrack() { 17 | // create a machine 18 | mach := am.New(context.Background(), am.Schema{ 19 | "A": {}, 20 | "B": {}, 21 | }, nil) 22 | 23 | // create a history 24 | h := Track(mach, am.S{"A", "C"}, 0) 25 | 26 | // collect some info 27 | h.ActivatedRecently("A", time.Second) // true 28 | fmt.Printf("%s", h.LastActivated["A"]) 29 | println(h.Entries) 30 | } 31 | 32 | func TestTrack(t *testing.T) { 33 | // create a machine 34 | mach := NewNoRels(t, nil) 35 | 36 | // create a history 37 | h := Track(mach, am.S{"A", "C"}, 0) 38 | 39 | // mutate 40 | mach.Add(am.S{"A", "B"}, nil) 41 | lastA := h.LastActivated["A"] 42 | mach.Add1("C", nil) 43 | mach.Remove1("A", nil) 44 | time.Sleep(time.Nanosecond) 45 | mach.Add(am.S{"A", "D"}, nil) 46 | 47 | // assert 48 | assert.Greater(t, h.LastActivated["A"].Nanosecond(), lastA.Nanosecond(), 49 | "Activation timestamp updated") 50 | assert.Len(t, h.Entries, 4, "4 mutations for tracked states") 51 | assert.True(t, h.ActivatedRecently("A", time.Second), "A was activated") 52 | assert.False(t, h.ActivatedRecently("B", time.Second), "B isn't tracked") 53 | } 54 | 55 | func TestTrackPreactivation(t *testing.T) { 56 | // create a machine 57 | mach := NewNoRels(t, am.S{"A", "C"}) 58 | 59 | // create a history 60 | h := Track(mach, am.S{"A", "C"}, 0) 61 | 62 | // assert 63 | assert.True(t, h.ActivatedRecently("A", time.Second), "A was pre-activated") 64 | } 65 | 66 | // NewNoRels creates a new machine with no relations between states. 67 | func NewNoRels(t *testing.T, initialState am.S) *am.Machine { 68 | m := am.New(context.Background(), am.Schema{ 69 | "A": {}, 70 | "B": {}, 71 | "C": {}, 72 | "D": {}, 73 | }, nil) 74 | m.SetLogger(func(i am.LogLevel, msg string, args ...any) { 75 | t.Logf(msg, args...) 76 | }) 77 | if amhelp.IsDebug() { 78 | m.SetLogLevel(am.LogEverything) 79 | m.HandlerTimeout = 2 * time.Minute 80 | } 81 | if initialState != nil { 82 | m.Set(initialState, nil) 83 | } 84 | return m 85 | } 86 | -------------------------------------------------------------------------------- /pkg/integrations/README.md: -------------------------------------------------------------------------------- 1 | # 🦾 /pkg/integrations 2 | 3 | [`cd /`](/README.md) 4 | 5 | > [!NOTE] 6 | > **asyncmachine-go** is a batteries-included graph control flow library (AOP, actor model, state-machine). 7 | 8 | **/pkg/integrations** is responsible for exposing state machines over various 9 | JSON transports, with currently only NATS being implemented. In the future, 10 | this may include email, Kafka, or HTTP. 11 | 12 | ## JSON 13 | 14 | JSON types cover **mutations**, **subscriptions**, and **data getters**. Each of these is divided in request and 15 | response objects which have a (/docs/jsonschema). Their usage depends on the specific implementation, eg in NATS each 16 | machine has a dedicated subtopic for mutation requests. 17 | 18 | - [`MutationReq`](/pkg/integrations/integrations.go) ([jsonschema](/docs/jsonschema/mutation_req.json)) 19 | - [`MutationResp`](/pkg/integrations/integrations.go) ([jsonschema](/docs/jsonschema/mutation_resp.json)) 20 | - [`WaitingReq`](/pkg/integrations/integrations.go) ([jsonschema](/docs/jsonschema/waiting_req.json)) 21 | - [`WaitingResp`](/pkg/integrations/integrations.go) ([jsonschema](/docs/jsonschema/waiting_resp.json)) 22 | - [`GetterReq`](/pkg/integrations/integrations.go) ([jsonschema](/docs/jsonschema/getter_req.json)) 23 | - [`GetterResp`](/pkg/integrations/integrations.go) ([jsonschema](/docs/jsonschema/getter_resp.json)) 24 | 25 | ```go 26 | import amjson "github.com/pancsta/asyncmachine-go/pkg/integration" 27 | 28 | // create a subscription to Foo 29 | reqSub := integrations.NewWaitingReq() 30 | reqSub.States = am.S{"Foo"} 31 | j, err := json.Marshal(reqSub) 32 | ``` 33 | 34 | ## NATS 35 | 36 | [NATS](https://github.com/nats-io/nats-server/) is a popular and high-performance messaging system made in Go. 37 | State machines are exposed under a **topic**, with each state machine also being subscribed to a dedicated subtopic 38 | "\[topic\].\[machineID\]" for mutation requests. Optional [queue] allows to load-balance requests across multiple 39 | subscribers. 40 | 41 | ```go 42 | import am "github.com/pancsta/asyncmachine-go" 43 | import nats "github.com/pancsta/asyncmachine-go/pkg/integration/nats" 44 | 45 | // ... 46 | 47 | var mach *am.Machine 48 | var ctx context.Context 49 | var nc *nats.Conn 50 | 51 | // expose mach under mytopic 52 | _ = nats.ExposeMachine(ctx, mach, nc, "mytopic", "") 53 | // mutate - add Foo 54 | res, _ := nats.Add(ctx, nc, topic, mach.Id(), am.S{"Foo"}, nil) 55 | if res == am.Executed { 56 | print("Foo added to mach") 57 | } 58 | ``` 59 | 60 | ## TODO 61 | 62 | - recipient matching (filters similar to the REPL ones) 63 | - better error handling (avoid overreporting) 64 | 65 | ## Status 66 | 67 | Alpha, work in progress, not semantically versioned. 68 | 69 | ## monorepo 70 | 71 | [Go back to the monorepo root](/README.md) to continue reading. 72 | -------------------------------------------------------------------------------- /pkg/node/states/ss_bootstrap.go: -------------------------------------------------------------------------------- 1 | package states 2 | 3 | import ( 4 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 5 | ssrpc "github.com/pancsta/asyncmachine-go/pkg/rpc/states" 6 | . "github.com/pancsta/asyncmachine-go/pkg/states/global" 7 | ) 8 | 9 | // BootstrapStatesDef contains all the states of the Bootstrap state machine. 10 | // The target state is WorkerAddr, activated by an aRPC client. 11 | type BootstrapStatesDef struct { 12 | 13 | // WorkerAddr - The awaited worker passed its connection details. 14 | WorkerAddr string 15 | 16 | // inherit from WorkerStatesDef 17 | *ssrpc.WorkerStatesDef 18 | } 19 | 20 | // BootstrapSchema represents all relations and properties of 21 | // BootstrapStatesDef. 22 | var BootstrapSchema = SchemaMerge( 23 | // inherit from WorkerStruct 24 | ssrpc.WorkerSchema, 25 | am.Schema{ 26 | cos.WorkerAddr: {}, 27 | }) 28 | 29 | // EXPORTS AND GROUPS 30 | 31 | var ( 32 | cos = am.NewStates(BootstrapStatesDef{}) 33 | 34 | // BootstrapStates contains all the states for the Bootstrap machine. 35 | BootstrapStates = cos 36 | ) 37 | -------------------------------------------------------------------------------- /pkg/node/states/ss_node_client.go: -------------------------------------------------------------------------------- 1 | package states 2 | 3 | import ( 4 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 5 | ssrpc "github.com/pancsta/asyncmachine-go/pkg/rpc/states" 6 | "github.com/pancsta/asyncmachine-go/pkg/states" 7 | . "github.com/pancsta/asyncmachine-go/pkg/states/global" 8 | ) 9 | 10 | // ClientStatesDef contains all the states of the Client state machine. 11 | type ClientStatesDef struct { 12 | *am.StatesBase 13 | 14 | Exception string 15 | ErrWorker string 16 | ErrSupervisor string 17 | 18 | // worker 19 | 20 | WorkerDisconnected string 21 | WorkerConnecting string 22 | WorkerConnected string 23 | WorkerDisconnecting string 24 | WorkerReady string 25 | // Ready - Client is connected to a worker and ready to delegate work and 26 | // receive payloads. 27 | Ready string 28 | 29 | // supervisor 30 | 31 | SuperDisconnected string 32 | SuperConnecting string 33 | SuperConnected string 34 | SuperDisconnecting string 35 | // SuperReady - Client is fully connected to the Supervisor. 36 | SuperReady string 37 | // WorkerRequested - Client has requested a Worker from the Supervisor. 38 | WorkerRequested string 39 | 40 | // inherit from BasicStatesDef 41 | *states.BasicStatesDef 42 | // inherit from ConsumerStatesDef 43 | *ssrpc.ConsumerStatesDef 44 | } 45 | 46 | // ClientGroupsDef contains all the state groups of the Client state machine. 47 | type ClientGroupsDef struct { 48 | *states.ConnectedGroupsDef 49 | // TODO? 50 | } 51 | 52 | // ClientSchema represents all relations and properties of ClientStates. 53 | var ClientSchema = SchemaMerge( 54 | // inherit from BasicStruct 55 | states.BasicSchema, 56 | // inherit from ConsumerStruct 57 | ssrpc.ConsumerSchema, 58 | am.Schema{ 59 | 60 | // errors 61 | 62 | ssC.ErrWorker: {Require: S{Exception}}, 63 | ssC.ErrSupervisor: {Require: S{Exception}}, 64 | 65 | // piped 66 | 67 | ssC.SuperDisconnected: {}, 68 | ssC.SuperConnecting: {}, 69 | ssC.SuperConnected: {}, 70 | ssC.SuperDisconnecting: {}, 71 | ssC.SuperReady: {}, 72 | 73 | ssC.WorkerDisconnected: {}, 74 | ssC.WorkerConnecting: {}, 75 | ssC.WorkerConnected: {}, 76 | ssC.WorkerDisconnecting: {}, 77 | ssC.WorkerReady: {Remove: S{ssC.WorkerRequested}}, 78 | 79 | // client 80 | 81 | ssC.WorkerRequested: {Require: S{ssC.SuperReady}}, 82 | ssC.Ready: { 83 | Auto: true, 84 | Require: S{ssC.WorkerReady}, 85 | }, 86 | }) 87 | 88 | // TODO handlers iface 89 | 90 | // EXPORTS AND GROUPS 91 | 92 | var ( 93 | ssC = am.NewStates(ClientStatesDef{}) 94 | sgC = am.NewStateGroups(ClientGroupsDef{}, states.ConnectedGroups) 95 | 96 | // ClientStates contains all the states for the Client machine. 97 | ClientStates = ssC 98 | // ClientGroups contains all the state groups for the Client machine. 99 | ClientGroups = sgC 100 | ) 101 | -------------------------------------------------------------------------------- /pkg/node/test/worker/node_test_worker.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "log/slog" 7 | "os" 8 | "time" 9 | 10 | testutils "github.com/pancsta/asyncmachine-go/internal/testing/utils" 11 | amhelp "github.com/pancsta/asyncmachine-go/pkg/helpers" 12 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 13 | "github.com/pancsta/asyncmachine-go/pkg/node" 14 | "github.com/pancsta/asyncmachine-go/pkg/node/states" 15 | "github.com/pancsta/asyncmachine-go/pkg/rpc" 16 | ) 17 | 18 | var ssW = states.WorkerStates 19 | 20 | func main() { 21 | ctx := context.Background() 22 | fA := flag.String("a", "", "addr") 23 | flag.Parse() 24 | if fA == nil || *fA == "" { 25 | panic("addr is required") 26 | } 27 | addr := *fA 28 | 29 | slog.Info("fork worker", "addr", addr) 30 | 31 | // worker 32 | 33 | // machine init 34 | mach := am.New(context.Background(), testutils.RelsNodeWorkerStruct, &am.Opts{ 35 | ID: "t-worker-" + addr}) 36 | err := mach.VerifyStates(testutils.RelsNodeWorkerStates) 37 | if err != nil { 38 | panic(err) 39 | } 40 | 41 | if os.Getenv(am.EnvAmDebug) != "" { 42 | mach.SetLogLevel(am.LogEverything) 43 | mach.HandlerTimeout = 2 * time.Minute 44 | } 45 | worker, err := node.NewWorker(ctx, "NTW", mach.Schema(), 46 | mach.StateNames(), nil) 47 | if err != nil { 48 | panic(err) 49 | } 50 | err = worker.Mach.BindHandlers(&workerHandlers{Mach: mach}) 51 | if err != nil { 52 | panic(err) 53 | } 54 | 55 | // connect Worker to the bootstrap machine 56 | res := worker.Start(addr) 57 | if res != am.Executed { 58 | panic(worker.Mach.Err()) 59 | } 60 | err = amhelp.WaitForAll(ctx, 1*time.Second, 61 | worker.Mach.When1(ssW.RpcReady, nil)) 62 | if err != nil { 63 | panic(err) 64 | } 65 | 66 | // wait for connection 67 | _ = amhelp.WaitForAll(ctx, 3*time.Second, 68 | worker.Mach.When1(ssW.SuperConnected, nil)) 69 | // block until disconnected 70 | <-worker.Mach.WhenNot1(ssW.SuperConnected, nil) 71 | } 72 | 73 | type workerHandlers struct { 74 | Mach *am.Machine 75 | } 76 | 77 | func (w *workerHandlers) WorkRequestedState(e *am.Event) { 78 | input := e.Args["input"].(int) 79 | 80 | payload := &rpc.ArgsPayload{ 81 | Name: w.Mach.Id(), 82 | Data: input * input, 83 | Source: e.Machine().Id(), 84 | } 85 | 86 | e.Machine().Add1(ssW.ClientSendPayload, rpc.PassRpc(&rpc.A{ 87 | Name: w.Mach.Id(), 88 | Payload: payload, 89 | })) 90 | } 91 | -------------------------------------------------------------------------------- /pkg/pubsub/README.md: -------------------------------------------------------------------------------- 1 | # /pkg/pubsub 2 | 3 | [`cd /`](/README.md) 4 | 5 | > [!NOTE] 6 | > **asyncmachine-go** is a batteries-included graph control flow library (AOP, actor model, state-machine). 7 | 8 | **/pkg/pubsub** is a trustful and decentralized synchronization network for asyncmachine-go. Each peer exposes several state 9 | machines, then starts gossiping about them and other ones known to him. Remote state machines are then visible to other 10 | peers as `/pkg/rpc.LocalWorker`. PubSub can be used to match Clients with Workers from [/pkg/node](/pkg/node/README.md). 11 | 12 | Under the hood it's based on [**libp2p gossipsub**](https://github.com/libp2p/go-libp2p-pubsub), which is a mesh-based 13 | PubSub, also based on gossipping, but for the purpose of network topology. **libp2p** gossips are separate from gossips 14 | of this package. 15 | 16 | ## Support 17 | 18 | - state checking YES 19 | - state mutations NO 20 | - state waiting YES 21 | 22 | ## Features 23 | 24 | - gossip-based discovery 25 | - gossip-based clock updates 26 | - gossip-based checksums via machine time 27 | - rate limitting 28 | - no leaders, no elections 29 | 30 | ## Screenshot 31 | 32 | [am-dbg](/tools/cmd/am-dbg/README.md) view of a PubSub with 6 peers, with p1-p5 exposing a single state machine each. 33 | 34 | ![](https://pancsta.github.io/assets/asyncmachine-go/am-dbg/pubsub.png) 35 | 36 | ## Schema 37 | 38 | State schema from [/pkg/pubsub/states/](/pkg/pubsub/states/ss_topic.go). 39 | 40 | ![worker schema](https://pancsta.github.io/assets/asyncmachine-go/schemas/pubsub.svg) 41 | 42 | ## TODO 43 | 44 | - more rate limiting 45 | - confirmed handler timeouts 46 | - faster discovery 47 | - load test 48 | - mDNS & DHT & auth 49 | - optimizations 50 | - documentation 51 | - discovery protocol 52 | - sequence diagrams 53 | 54 | ## Usage 55 | 56 | ```go 57 | import ( 58 | ma "github.com/multiformats/go-multiaddr" 59 | ampubsub "github.com/pancsta/asyncmachine-go/pkg/pubsub" 60 | ) 61 | 62 | // ... 63 | 64 | // init a pubsub peer 65 | ps, _ := ampubsub.NewTopic(ctx, t.Name(), name, machs, nil) 66 | // prep a libp2p multi address 67 | a, _ := ma.NewMultiaddr("/ip4/127.0.0.1/udp/75343/quic-v1") 68 | addrs := []ma.Multiaddr{a} 69 | ps.ConnAddrs = addrs 70 | ps.Start() 71 | <-ps.Mach.When1(ss.Connected, ctx) 72 | ps.Mach.Add1() 73 | ``` 74 | 75 | ## Status 76 | 77 | Alpha, work in progress, not semantically versioned. 78 | 79 | ## Credits 80 | 81 | - [libp2p](https://libp2p.io/) 82 | 83 | ## monorepo 84 | 85 | [Go back to the monorepo root](/README.md) to continue reading. 86 | -------------------------------------------------------------------------------- /pkg/rpc/rpcnames/rpcnames.go: -------------------------------------------------------------------------------- 1 | package rpcnames 2 | 3 | // TODO use enum pkg github.com/xybor-x/enum 4 | // TODO separate ClientNames and ServerNames 5 | // TODO keep in /pkg/rpc? 6 | 7 | type Name int 8 | 9 | const ( 10 | // Server 11 | 12 | Add Name = iota + 1 13 | AddNS 14 | Remove 15 | Set 16 | Hello 17 | Handshake 18 | Log 19 | Sync 20 | Bye 21 | 22 | // Client 23 | 24 | ClientSetClock 25 | ClientPushAllTicks 26 | ClientSendPayload 27 | ClientBye 28 | ) 29 | 30 | func (n Name) Encode() string { 31 | return string(rune(n)) 32 | } 33 | 34 | func (n Name) String() string { 35 | switch n { 36 | case Add: 37 | return "Add" 38 | case AddNS: 39 | return "AddNS" 40 | case Remove: 41 | return "Remove" 42 | case Set: 43 | return "Set" 44 | case Hello: 45 | return "Hello" 46 | case Handshake: 47 | return "Handshake" 48 | case Log: 49 | return "Log" 50 | case Sync: 51 | return "Sync" 52 | case Bye: 53 | return "Close" 54 | case ClientSetClock: 55 | return "ClientSetClock" 56 | case ClientPushAllTicks: 57 | return "ClientPushAllTicks" 58 | case ClientSendPayload: 59 | return "ClientSendPayload" 60 | case ClientBye: 61 | return "ClientBye" 62 | } 63 | 64 | return "!UNKNOWN!" 65 | } 66 | 67 | func Decode(s string) Name { 68 | return Name(s[0]) 69 | } 70 | -------------------------------------------------------------------------------- /pkg/rpc/states/ss_mux.go: -------------------------------------------------------------------------------- 1 | package states 2 | 3 | import ( 4 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 5 | "github.com/pancsta/asyncmachine-go/pkg/states" 6 | . "github.com/pancsta/asyncmachine-go/pkg/states/global" 7 | ) 8 | 9 | // MuxStatesDef contains all the states of the Mux state machine. 10 | // The target state is PortInfo, activated by an aRPC client. 11 | type MuxStatesDef struct { 12 | // shadow duplicated StatesBase 13 | *am.StatesBase 14 | 15 | // basics 16 | 17 | // Ready - mux is ready to accept new clients. 18 | Ready string 19 | 20 | ClientConnected string 21 | HasClients string 22 | // NewServerErr - new server returned an error. The mux is still running. 23 | NewServerErr string 24 | 25 | // inherit from BasicStatesDef 26 | *states.BasicStatesDef 27 | } 28 | 29 | // MuxSchema represents all relations and properties of MuxStatesDef. 30 | var MuxSchema = SchemaMerge( 31 | states.BasicSchema, 32 | am.Schema{ 33 | ssD.Exception: { 34 | Multi: true, 35 | Remove: S{ssS.Ready}, 36 | }, 37 | 38 | ssD.Ready: { 39 | Require: S{ssS.Start}, 40 | }, 41 | 42 | ssD.ClientConnected: { 43 | Multi: true, 44 | Require: states.S{ssD.Start}, 45 | }, 46 | ssD.HasClients: {Require: states.S{ssD.Start}}, 47 | ssD.NewServerErr: {}, 48 | }) 49 | 50 | // EXPORTS AND GROUPS 51 | 52 | var ( 53 | ssD = am.NewStates(MuxStatesDef{}) 54 | 55 | // MuxStates contains all the states for the Mux machine. 56 | MuxStates = ssD 57 | ) 58 | -------------------------------------------------------------------------------- /pkg/rpc/states/ss_rpc_client.go: -------------------------------------------------------------------------------- 1 | package states 2 | 3 | import ( 4 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 5 | "github.com/pancsta/asyncmachine-go/pkg/states" 6 | . "github.com/pancsta/asyncmachine-go/pkg/states/global" 7 | ) 8 | 9 | // ClientStatesDef contains all the states of the Client state machine. 10 | type ClientStatesDef struct { 11 | // shadow duplicated StatesBase 12 | *am.StatesBase 13 | 14 | // failsafe 15 | 16 | RetryingCall string 17 | // TODO should be ErrCallRetry, req:Exception 18 | CallRetryFailed string 19 | RetryingConn string 20 | // TODO should be ErrConnRetry, req:Exception 21 | ConnRetryFailed string 22 | 23 | // local docs 24 | 25 | // Ready indicates the remote worker is ready to be used. 26 | Ready string 27 | 28 | // worker delivers 29 | 30 | // WorkerDelivering is an optional indication that the server has started a 31 | // data transmission to the Client. 32 | WorkerDelivering string 33 | // WorkPayload allows the Consumer to bind his handlers and receive data 34 | // from the Client. 35 | WorkerPayload string 36 | 37 | // inherit from SharedStatesDef 38 | *SharedStatesDef 39 | // inherit from ConnectedStatesDef 40 | *states.ConnectedStatesDef 41 | } 42 | 43 | // ClientGroupsDef contains all the state groups of the Client state machine. 44 | type ClientGroupsDef struct { 45 | *SharedGroupsDef 46 | *states.ConnectedGroupsDef 47 | // TODO 48 | } 49 | 50 | // ClientSchema represents all relations and properties of ClientStates. 51 | var ClientSchema = SchemaMerge( 52 | // inherit from SharedStruct 53 | SharedSchema, 54 | // inherit from ConnectedStruct 55 | states.ConnectedSchema, 56 | am.Schema{ 57 | 58 | // Try to RetryingConn on ErrNetwork. 59 | ssC.ErrNetwork: { 60 | Require: S{am.Exception}, 61 | Remove: S{ssC.Connecting}, 62 | }, 63 | 64 | ssC.Start: { 65 | Add: S{ssC.Connecting}, 66 | Remove: S{ssC.ConnRetryFailed}, 67 | }, 68 | ssC.Ready: { 69 | Auto: true, 70 | Require: S{ssC.HandshakeDone}, 71 | }, 72 | 73 | // inject Client states into Connected 74 | ssC.Connected: StateAdd( 75 | states.ConnectedSchema[states.ConnectedStates.Connected], 76 | am.State{ 77 | Remove: S{ssC.RetryingConn}, 78 | Add: S{ssC.Handshaking}, 79 | }), 80 | 81 | // inject Client states into Handshaking 82 | ssC.Handshaking: StateAdd( 83 | SharedSchema[s.Handshaking], 84 | am.State{ 85 | Require: S{ssC.Connected}, 86 | }), 87 | 88 | // inject Client states into HandshakeDone 89 | ssC.HandshakeDone: am.StateAdd( 90 | SharedSchema[ssC.HandshakeDone], am.State{ 91 | // HandshakeDone will depend on Connected. 92 | Require: S{ssC.Connected}, 93 | }), 94 | 95 | // Retrying 96 | 97 | ssC.RetryingCall: {Require: S{ssC.Start}}, 98 | ssC.CallRetryFailed: { 99 | Remove: S{ssC.RetryingCall}, 100 | Add: S{ssC.ErrNetwork, am.Exception}, 101 | }, 102 | ssC.RetryingConn: {Require: S{ssC.Start}}, 103 | ssC.ConnRetryFailed: {Remove: S{ssC.Start}}, 104 | 105 | // worker delivers 106 | 107 | ssC.WorkerDelivering: { 108 | Multi: true, 109 | Require: S{ssC.Connected}, 110 | }, 111 | ssC.WorkerPayload: { 112 | Multi: true, 113 | Require: S{ssC.Connected}, 114 | }, 115 | }) 116 | 117 | // EXPORTS AND GROUPS 118 | 119 | var ( 120 | ssC = am.NewStates(ClientStatesDef{}) 121 | sgC = am.NewStateGroups(ClientGroupsDef{}, states.ConnectedGroups, 122 | SharedGroups) 123 | 124 | // ClientStates contains all the states for the Client machine. 125 | ClientStates = ssC 126 | // ClientGroups contains all the state groups for the Client machine. 127 | ClientGroups = sgC 128 | ) 129 | -------------------------------------------------------------------------------- /pkg/rpc/states/ss_rpc_consumer.go: -------------------------------------------------------------------------------- 1 | package states 2 | 3 | import ( 4 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 5 | ) 6 | 7 | // ConsumerStatesDef contains all the states of the Consumer state machine. 8 | type ConsumerStatesDef struct { 9 | *am.StatesBase 10 | Exception string 11 | 12 | // WorkerPayload RPC server delivers the requested payload to the Client. 13 | WorkerPayload string 14 | } 15 | 16 | // ConsumerSchema represents all relations and properties of ConsumerStates. 17 | var ConsumerSchema = am.Schema{ 18 | ssCo.WorkerPayload: {Multi: true}, 19 | } 20 | 21 | // ConsumerHandlers is the required interface for Consumer's state handlers. 22 | type ConsumerHandlers interface { 23 | WorkerPayloadState(e *am.Event) 24 | } 25 | 26 | // EXPORTS AND GROUPS 27 | 28 | var ( 29 | // ssCo is Consumer states from ConsumerStatesDef. 30 | ssCo = am.NewStates(ConsumerStatesDef{}) 31 | 32 | // ConsumerStates contains all the states for the Consumer machine. 33 | ConsumerStates = ssCo 34 | ) 35 | -------------------------------------------------------------------------------- /pkg/rpc/states/ss_rpc_server.go: -------------------------------------------------------------------------------- 1 | package states 2 | 3 | import ( 4 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 5 | . "github.com/pancsta/asyncmachine-go/pkg/states/global" 6 | ) 7 | 8 | // ServerStatesDef contains all the states of the Client state machine. 9 | type ServerStatesDef struct { 10 | 11 | // errors 12 | 13 | // ErrOnClient indicates an error added on the RPC worker, not the source 14 | // worker. 15 | ErrOnClient string 16 | 17 | // basics 18 | 19 | // Ready - Client is fully connected to the server. 20 | Ready string 21 | 22 | // rpc 23 | 24 | RpcStarting string 25 | RpcReady string 26 | 27 | // TODO failsafe 28 | // RetryingCall string 29 | // CallRetryFailed string 30 | 31 | ClientConnected string 32 | 33 | // inherit from SharedStatesDef 34 | *SharedStatesDef 35 | } 36 | 37 | // ServerGroupsDef contains all the state groups of the Client state machine. 38 | type ServerGroupsDef struct { 39 | *SharedGroupsDef 40 | 41 | // Rpc is a group for RPC ready states. 42 | Rpc S 43 | } 44 | 45 | // ServerSchema represents all relations and properties of ClientStates. 46 | var ServerSchema = SchemaMerge( 47 | // inherit from SharedStruct 48 | SharedSchema, 49 | am.Schema{ 50 | 51 | ssS.ErrOnClient: {Require: S{Exception}}, 52 | ssS.ErrNetwork: { 53 | Require: S{am.Exception}, 54 | Remove: S{ssS.ClientConnected}, 55 | }, 56 | 57 | // inject Server states into HandshakeDone 58 | ssS.HandshakeDone: StateAdd( 59 | SharedSchema[ssS.HandshakeDone], 60 | am.State{ 61 | Remove: S{Exception}, 62 | }), 63 | 64 | // Server 65 | 66 | ssS.Start: {Add: S{ssS.RpcStarting}}, 67 | ssS.Ready: { 68 | Auto: true, 69 | Require: S{ssS.HandshakeDone, ssS.RpcReady}, 70 | }, 71 | 72 | ssS.RpcStarting: { 73 | Require: S{ssS.Start}, 74 | Remove: sgS.Rpc, 75 | }, 76 | ssS.RpcReady: { 77 | Require: S{ssS.Start}, 78 | Remove: sgS.Rpc, 79 | }, 80 | 81 | ssS.ClientConnected: { 82 | Require: S{ssS.RpcReady}, 83 | }, 84 | // TODO ClientBye for graceful shutdowns 85 | }) 86 | 87 | // EXPORTS AND GROUPS 88 | 89 | var ( 90 | ssS = am.NewStates(ServerStatesDef{}) 91 | sgS = am.NewStateGroups(ServerGroupsDef{ 92 | Rpc: S{ssS.RpcStarting, ssS.RpcReady}, 93 | }, SharedGroups) 94 | 95 | // ServerStates contains all the states for the Client machine. 96 | ServerStates = ssS 97 | // ServerGroups contains all the state groups for the Client machine. 98 | ServerGroups = sgS 99 | ) 100 | -------------------------------------------------------------------------------- /pkg/rpc/states/ss_rpc_shared.go: -------------------------------------------------------------------------------- 1 | package states 2 | 3 | import ( 4 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 5 | "github.com/pancsta/asyncmachine-go/pkg/states" 6 | . "github.com/pancsta/asyncmachine-go/pkg/states/global" 7 | ) 8 | 9 | // SharedStatesDef contains all the states of the Worker state machine. 10 | type SharedStatesDef struct { 11 | 12 | // errors 13 | 14 | ErrNetworkTimeout string 15 | ErrRpc string 16 | ErrDelivery string 17 | 18 | // connection 19 | 20 | HandshakeDone string 21 | Handshaking string 22 | 23 | // inherit from BasicStatesDef 24 | *states.BasicStatesDef 25 | } 26 | 27 | // SharedGroupsDef contains all the state groups of the Worker state machine. 28 | type SharedGroupsDef struct { 29 | 30 | // Work represents work-related states, 1 active at a time. 31 | Handshake S 32 | } 33 | 34 | // SharedSchema represents all relations and properties of WorkerStates. 35 | var SharedSchema = SchemaMerge( 36 | // inherit from BasicStruct 37 | states.BasicSchema, 38 | am.Schema{ 39 | 40 | // Errors 41 | s.ErrNetworkTimeout: { 42 | Add: S{s.Exception}, 43 | Require: S{s.Exception}, 44 | }, 45 | s.ErrRpc: { 46 | Add: S{s.Exception}, 47 | Require: S{s.Exception}, 48 | }, 49 | s.ErrDelivery: { 50 | Add: S{s.Exception}, 51 | Require: S{s.Exception}, 52 | }, 53 | 54 | // Handshake 55 | s.Handshaking: { 56 | Require: S{s.Start}, 57 | Remove: g.Handshake, 58 | }, 59 | s.HandshakeDone: { 60 | Require: S{s.Start}, 61 | Remove: g.Handshake, 62 | }, 63 | }) 64 | 65 | // EXPORTS AND GROUPS 66 | 67 | var ( 68 | // ws is worker states from SharedStatesDef. 69 | s = am.NewStates(SharedStatesDef{}) 70 | 71 | // wg is worker groups from SharedGroupsDef. 72 | g = am.NewStateGroups(SharedGroupsDef{ 73 | Handshake: S{s.Handshaking, s.HandshakeDone}, 74 | }) 75 | 76 | // SharedStates contains all the states shared RPC states. 77 | SharedStates = s 78 | 79 | // SharedGroups contains all the shared state groups for RPC. 80 | SharedGroups = g 81 | ) 82 | -------------------------------------------------------------------------------- /pkg/rpc/states/ss_rpc_worker.go: -------------------------------------------------------------------------------- 1 | package states 2 | 3 | import ( 4 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 5 | "github.com/pancsta/asyncmachine-go/pkg/states" 6 | . "github.com/pancsta/asyncmachine-go/pkg/states/global" 7 | ) 8 | 9 | // WorkerStatesDef contains all the states of the Worker state machine. 10 | type WorkerStatesDef struct { 11 | 12 | // errors 13 | 14 | // ErrProviding - Worker had issues providing the requested payload. 15 | ErrProviding string 16 | // ErrSendPayload - RPC server had issues sending the requested payload to 17 | // the RPC client. 18 | ErrSendPayload string 19 | 20 | // rpc getter 21 | 22 | // SendPayload - Worker delivered requested payload to the RPC server using 23 | // rpc.Pass, rpc.A, and rpc.ArgsPayload. 24 | SendPayload string 25 | 26 | // inherit from BasicStatesDef 27 | *states.BasicStatesDef 28 | } 29 | 30 | // WorkerSchema represents all relations and properties of WorkerStates. 31 | var WorkerSchema = SchemaMerge( 32 | // inherit from BasicStruct 33 | states.BasicSchema, 34 | am.Schema{ 35 | 36 | // errors 37 | 38 | ssW.ErrProviding: {Require: S{am.Exception}}, 39 | ssW.ErrSendPayload: {Require: S{am.Exception}}, 40 | 41 | // rcp getter 42 | 43 | ssW.SendPayload: {Multi: true}, 44 | }) 45 | 46 | // EXPORTS AND GROUPS 47 | 48 | var ( 49 | // ssW is worker states from WorkerStatesDef. 50 | ssW = am.NewStates(WorkerStatesDef{}) 51 | 52 | // WorkerStates contains all the states for the Worker machine. 53 | WorkerStates = ssW 54 | ) 55 | -------------------------------------------------------------------------------- /pkg/rpc/utils_test.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/lithammer/dedent" 8 | "github.com/stretchr/testify/assert" 9 | 10 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 11 | ) 12 | 13 | // TOOD use /internal 14 | 15 | func assertStates(t *testing.T, m am.Api, expected am.S, 16 | msgAndArgs ...interface{}, 17 | ) { 18 | // TODO ignore Healthcheck 19 | assert.ElementsMatch(t, expected, m.ActiveStates(), msgAndArgs...) 20 | } 21 | 22 | func assertTime(t *testing.T, m am.Api, states am.S, time am.Time, 23 | msgAndArgs ...interface{}, 24 | ) { 25 | assert.Subset(t, m.Time(states), time, msgAndArgs...) 26 | } 27 | 28 | func assertString( 29 | t *testing.T, m am.Api, expected string, states am.S, 30 | ) { 31 | assert.Equal(t, 32 | strings.Trim(dedent.Dedent(expected), "\n"), 33 | strings.Trim(m.Inspect(states), "\n")) 34 | } 35 | -------------------------------------------------------------------------------- /pkg/states/global/states_utils.go: -------------------------------------------------------------------------------- 1 | // Package global should be imported into the package's global scope with: 2 | // 3 | // import _ "github.com/pancsta/asyncmachine-go/pkg/states/global" 4 | // 5 | // This removes the need for manual updates, with the cost of an implicit 6 | // import. 7 | package global 8 | 9 | import am "github.com/pancsta/asyncmachine-go/pkg/machine" 10 | 11 | // S is a type alias for a list of state names. 12 | type S = am.S 13 | 14 | // State is a type alias for a state definition. See [am.State]. 15 | type State = am.State 16 | 17 | // SAdd is a func alias for merging lists of states. 18 | var SAdd = am.SAdd 19 | 20 | // StateAdd is a func alias for adding to an existing state definition. 21 | var StateAdd = am.StateAdd 22 | 23 | // StateSet is a func alias for replacing parts of an existing state 24 | // definition. 25 | var StateSet = am.StateSet 26 | 27 | // SchemaMerge is a func alias for extending an existing state schema. 28 | var SchemaMerge = am.SchemaMerge 29 | 30 | // Exception is a type alias for the exception state. 31 | var Exception = am.Exception 32 | -------------------------------------------------------------------------------- /pkg/states/ss_basic.go: -------------------------------------------------------------------------------- 1 | // Package states provides reusable state definitions. 2 | // 3 | // - basic 4 | // - connected 5 | // - disposed 6 | package states 7 | 8 | import ( 9 | _ "embed" 10 | 11 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 12 | ) 13 | 14 | //go:embed states_utils.go 15 | var StatesUtilsFile string 16 | 17 | // BasicStatesDef contains all the basic states. 18 | type BasicStatesDef struct { 19 | *am.StatesBase 20 | 21 | // ErrNetwork indicates a generic network error. 22 | ErrNetwork string 23 | // ErrHandlerTimeout indicates one of state machine handlers has timed out. 24 | ErrHandlerTimeout string 25 | 26 | // Start indicates the machine should be working. Removing start can force 27 | // stop the machine. 28 | Start string 29 | // Ready indicates the machine meets criteria to perform work. 30 | Ready string 31 | // Healthcheck is a periodic request making sure that the machine is still 32 | // alive. 33 | Healthcheck string 34 | // Heartbeat is a periodic state which ensures integrity of the machine. 35 | Heartbeat string 36 | } 37 | 38 | var BasicSchema = am.Schema{ 39 | // Errors 40 | 41 | ssB.Exception: {Multi: true}, 42 | ssB.ErrNetwork: {Require: S{Exception}}, 43 | ssB.ErrHandlerTimeout: {Require: S{Exception}}, 44 | 45 | // Basics 46 | 47 | ssB.Start: {}, 48 | ssB.Ready: {Require: S{ssB.Start}}, 49 | ssB.Healthcheck: {Multi: true}, 50 | ssB.Heartbeat: {}, 51 | } 52 | 53 | // EXPORTS AND GROUPS 54 | 55 | var ( 56 | ssB = am.NewStates(BasicStatesDef{}) 57 | 58 | // BasicStates contains all the states for the Basic machine. 59 | BasicStates = ssB 60 | ) 61 | -------------------------------------------------------------------------------- /pkg/states/ss_disposed.go: -------------------------------------------------------------------------------- 1 | package states 2 | 3 | import ( 4 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 5 | ) 6 | 7 | // DisposedStatesDef contains all the states of the Disposed state machine. 8 | // Required states: 9 | // - Start 10 | type DisposedStatesDef struct { 11 | *am.StatesBase 12 | 13 | // RegisterDisposal registers a disposal handler passed under the 14 | // DisposedArgHandler key. 15 | RegisterDisposal string 16 | // Disposing indicates that the machine is during the disposal process. 17 | Disposing string 18 | // Disposed indicates that the machine has disposed allocated resoruces 19 | // and is ready to be garbage collected by calling [am.Machine.Dispose]. 20 | Disposed string 21 | } 22 | 23 | // DisposedGroupsDef contains all the state groups Disposed state machine. 24 | type DisposedGroupsDef struct { 25 | Disposed S 26 | } 27 | 28 | // DisposedSchema represents all relations and properties of DisposedStates. 29 | var DisposedSchema = am.Schema{ 30 | ssD.RegisterDisposal: {Multi: true}, 31 | ssD.Disposing: {Remove: sgD.Disposed}, 32 | ssD.Disposed: {Remove: SAdd(sgD.Disposed, S{ssB.Start})}, 33 | } 34 | 35 | // EXPORTS AND GROUPS 36 | 37 | var ( 38 | ssD = am.NewStates(DisposedStatesDef{}) 39 | sgD = am.NewStateGroups(DisposedGroupsDef{ 40 | Disposed: S{ssD.RegisterDisposal, ssD.Disposing, ssD.Disposed}, 41 | }) 42 | 43 | // DisposedStates contains all the states for the Disposed machine. 44 | DisposedStates = ssD 45 | // DisposedGroups contains all the state groups for the Disposed machine. 46 | DisposedGroups = sgD 47 | ) 48 | 49 | // handlers 50 | 51 | var DisposedArgHandler = "DisposedArgHandler" 52 | 53 | type DisposedHandlers struct { 54 | // DisposedHandlers is a list of handler for pkg/states.DisposedStates 55 | DisposedHandlers []am.HandlerDispose 56 | } 57 | 58 | func (h *DisposedHandlers) RegisterDisposalEnter(e *am.Event) bool { 59 | fn, ok := e.Args[DisposedArgHandler].(am.HandlerDispose) 60 | return ok && fn != nil 61 | } 62 | 63 | func (h *DisposedHandlers) RegisterDisposalState(e *am.Event) { 64 | fn := e.Args[DisposedArgHandler].(am.HandlerDispose) 65 | h.DisposedHandlers = append(h.DisposedHandlers, fn) 66 | } 67 | 68 | func (h *DisposedHandlers) DisposingState(e *am.Event) { 69 | mach := e.Machine() 70 | ctx := mach.NewStateCtx(ssD.Disposing) 71 | 72 | // unblock 73 | go func() { 74 | for _, fn := range h.DisposedHandlers { 75 | if ctx.Err() != nil { 76 | return // expired 77 | } 78 | fn(mach.Id(), ctx) 79 | } 80 | 81 | mach.Add1(ssD.Disposed, nil) 82 | }() 83 | } 84 | -------------------------------------------------------------------------------- /pkg/states/states_utils.go: -------------------------------------------------------------------------------- 1 | package states 2 | 3 | import am "github.com/pancsta/asyncmachine-go/pkg/machine" 4 | 5 | // S is a type alias for a list of state names. 6 | type S = am.S 7 | 8 | // State is a type alias for a state definition. See [am.State]. 9 | type State = am.State 10 | 11 | // SAdd is a func alias for merging lists of states. 12 | var SAdd = am.SAdd 13 | 14 | // StateAdd is a func alias for adding to an existing state definition. 15 | var StateAdd = am.StateAdd 16 | 17 | // StateSet is a func alias for replacing parts of an existing state 18 | // definition. 19 | var StateSet = am.StateSet 20 | 21 | // SchemaMerge is a func alias for extending an existing state structure. 22 | var SchemaMerge = am.SchemaMerge 23 | 24 | // Exception is a type alias for the exception state. 25 | var Exception = am.Exception 26 | -------------------------------------------------------------------------------- /pkg/telemetry/otel_test.go: -------------------------------------------------------------------------------- 1 | package telemetry 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "go.opentelemetry.io/otel/exporters/stdout/stdoutlog" 9 | 10 | ss "github.com/pancsta/asyncmachine-go/internal/testing/states" 11 | testutils "github.com/pancsta/asyncmachine-go/internal/testing/utils" 12 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 13 | ) 14 | 15 | func TestLog(t *testing.T) { 16 | var buf strings.Builder 17 | logExporter, err := stdoutlog.New(stdoutlog.WithWriter(&buf)) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | logProvider := NewOtelLoggerProvider(logExporter) 22 | mach := testutils.NewRels(t, nil) 23 | mach.SetLogLevel(am.LogDecisions) 24 | BindOtelLogger(mach, logProvider, "") 25 | 26 | mach.Add1(ss.C, nil) 27 | mach.Remove1(ss.A, nil) 28 | 29 | err = logProvider.ForceFlush(mach.Ctx()) 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | 34 | out := buf.String() 35 | 36 | assert.Contains(t, out, `"Value":"t-TestLog"`) 37 | assert.Contains(t, out, "[queue:add] C") 38 | assert.Contains(t, out, "[state:auto] +A") 39 | } 40 | -------------------------------------------------------------------------------- /pkg/telemetry/telemetry.go: -------------------------------------------------------------------------------- 1 | package telemetry 2 | 3 | import ( 4 | "os" 5 | "regexp" 6 | "strings" 7 | 8 | "github.com/ic2hrmk/promtail" 9 | 10 | ssam "github.com/pancsta/asyncmachine-go/pkg/states" 11 | 12 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 13 | ) 14 | 15 | const ( 16 | EnvService = "AM_SERVICE" 17 | EnvLokiAddr = "AM_LOKI_ADDR" 18 | EnvOtelTrace = "AM_OTEL_TRACE" 19 | EnvOtelTraceTxs = "AM_OTEL_TRACE_TXS" 20 | EnvOtelTraceArgs = "AM_OTEL_TRACE_ARGS" 21 | EnvOtelTraceNoauto = "AM_OTEL_TRACE_NOAUTO" 22 | ) 23 | 24 | func BindLokiLogger(mach am.Api, client promtail.Client) { 25 | labels := map[string]string{ 26 | "asyncmachine_id": mach.Id(), 27 | } 28 | mach.SetLogId(false) 29 | 30 | amlog := func(level am.LogLevel, msg string, args ...any) { 31 | if strings.HasPrefix(msg, "[error") { 32 | client.LogfWithLabels(promtail.Error, labels, msg, args...) 33 | } else { 34 | switch level { 35 | 36 | case am.LogChanges: 37 | client.LogfWithLabels(promtail.Info, labels, msg, args...) 38 | case am.LogOps: 39 | client.LogfWithLabels(promtail.Info, labels, msg, args...) 40 | case am.LogDecisions: 41 | client.LogfWithLabels(promtail.Debug, labels, msg, args...) 42 | case am.LogEverything: 43 | client.LogfWithLabels(promtail.Debug, labels, msg, args...) 44 | default: 45 | } 46 | } 47 | } 48 | 49 | mach.SetLogger(amlog) 50 | mach.Log("[bind] loki logger") 51 | } 52 | 53 | // everything else than a-z and _ 54 | var normalizeRegexp = regexp.MustCompile("[^a-z_0-9]+") 55 | 56 | func NormalizeId(id string) string { 57 | return normalizeRegexp.ReplaceAllString(strings.ToLower(id), "_") 58 | } 59 | 60 | // BindLokiEnv bind Loki logger to [mach], based on environment vars: 61 | // - AM_SERVICE (required) 62 | // - AM_LOKI_ADDR (required) 63 | // This tracer is NOT inherited by submachines. 64 | func BindLokiEnv(mach am.Api) error { 65 | service := os.Getenv(EnvService) 66 | addr := os.Getenv(EnvLokiAddr) 67 | if service == "" || addr == "" { 68 | return nil 69 | } 70 | 71 | // init promtail and bind AM logger 72 | identifiers := map[string]string{ 73 | "service_name": NormalizeId(service), 74 | } 75 | pt, err := promtail.NewJSONv1Client(addr, identifiers) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | // dispose somehow 81 | register := ssam.DisposedStates.RegisterDisposal 82 | if mach.Has1(register) { 83 | mach.Add1(register, am.A{ 84 | ssam.DisposedArgHandler: pt.Close, 85 | }) 86 | } else { 87 | func() { 88 | <-mach.WhenDisposed() 89 | pt.Close() 90 | }() 91 | } 92 | 93 | BindLokiLogger(mach, pt) 94 | 95 | return nil 96 | } 97 | -------------------------------------------------------------------------------- /pkg/x/helpers/x_helpers_test.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | amhelp "github.com/pancsta/asyncmachine-go/pkg/helpers" 9 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | type S = am.S 14 | type T = am.Time 15 | 16 | func TestTimeMatrix(t *testing.T) { 17 | // m1 init 18 | m1 := NewNoRels(t, nil) 19 | statesStruct := m1.Schema() 20 | statesStruct["B"] = am.State{Multi: true} 21 | err := m1.SetSchema(statesStruct, S{"A", "B", "C", "D", am.Exception}) 22 | assert.NoError(t, err) 23 | 24 | // mutate & assert 25 | m1.Add(S{"A", "B"}, nil) 26 | m1.Add(S{"A", "B"}, nil) 27 | assertTime(t, m1, S{"A", "B", "C", "D"}, T{1, 3, 0, 0}, 28 | "m1 clocks mismatch") 29 | 30 | // m2 init 31 | m2 := NewNoRels(t, nil) 32 | statesStruct = m1.Schema() 33 | statesStruct["B"] = am.State{Multi: true} 34 | err = m2.SetSchema(statesStruct, S{"A", "B", "C", "D", am.Exception}) 35 | assert.NoError(t, err) 36 | 37 | // mutate & assert 38 | m2.Add(S{"A", "B"}, nil) 39 | m2.Add(S{"A", "B"}, nil) 40 | m2.Add(S{"A", "B", "C"}, nil) 41 | m2.Set(S{"D"}, nil) 42 | assertTime(t, m2, S{"A", "B", "C", "D"}, T{2, 6, 2, 1}, 43 | "m2 clocks mismatch") 44 | 45 | matrix, err := TimeMatrix([]*am.Machine{m1, m2}) 46 | assert.NoError(t, err) 47 | assert.Equal(t, T{1, 3, 0, 0, 0}, matrix[0]) 48 | assert.Equal(t, T{2, 6, 2, 1, 0}, matrix[1]) 49 | } 50 | 51 | // /// helpers 52 | // TODO extract test helpers to internal/testing 53 | 54 | // NewNoRels creates a new machine with no relations between states. 55 | func NewNoRels(t *testing.T, initialState am.S) *am.Machine { 56 | m := am.New(context.Background(), am.Schema{ 57 | "A": {}, 58 | "B": {}, 59 | "C": {}, 60 | "D": {}, 61 | }, nil) 62 | m.SetLogger(func(i am.LogLevel, msg string, args ...any) { 63 | t.Logf(msg, args...) 64 | }) 65 | if amhelp.IsDebug() { 66 | m.SetLogLevel(am.LogEverything) 67 | m.HandlerTimeout = 2 * time.Minute 68 | } 69 | if initialState != nil { 70 | m.Set(initialState, nil) 71 | } 72 | return m 73 | } 74 | 75 | func assertTime(t *testing.T, m *am.Machine, states S, time T, 76 | msgAndArgs ...interface{}, 77 | ) { 78 | assert.Equal(t, m.Time(states), time, msgAndArgs...) 79 | } 80 | -------------------------------------------------------------------------------- /scripts/compact_number/compact_number.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | 8 | "github.com/nkall/compactnumber" 9 | ) 10 | 11 | func main() { 12 | var input string 13 | 14 | if len(os.Args) > 1 { 15 | input = os.Args[1] 16 | } else { 17 | fmt.Scanln(&input) //nolint:errcheck 18 | } 19 | 20 | number, err := strconv.Atoi(input) 21 | if err != nil { 22 | fmt.Printf("Invalid number: %s\n", input) 23 | return 24 | } 25 | 26 | formatter := compactnumber.NewFormatter("en-US", compactnumber.Short) 27 | out, err := formatter.Format(number) 28 | if err != nil { 29 | panic(err) 30 | } 31 | 32 | fmt.Println(out) 33 | } -------------------------------------------------------------------------------- /scripts/dep-taskfile.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # check if installed 4 | if command -v task >/dev/null; then echo "OK" && exit; fi 5 | 6 | echo "Visit https://taskfile.dev/installation/ for more info" 7 | echo "----- ----- -----" 8 | 9 | sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b ~/.local/bin 10 | -------------------------------------------------------------------------------- /scripts/extract_mermaid/extract_mermaid.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "regexp" 8 | "slices" 9 | "strings" 10 | ) 11 | 12 | func main() { 13 | err := extractMermaidDiagrams(os.Args[1], os.Args[2]) 14 | if err != nil { 15 | fmt.Println("Error:", err) 16 | } 17 | } 18 | 19 | func extractMermaidDiagrams(inputFile string, reqIndexes string) error { 20 | // Open the Markdown file 21 | file, err := os.Open(inputFile) 22 | if err != nil { 23 | return fmt.Errorf("failed to open file: %v", err) 24 | } 25 | defer file.Close() 26 | 27 | ri := strings.Split(reqIndexes, " ") 28 | 29 | // Regex to detect mermaid code blocks 30 | startRegex := regexp.MustCompile("^```mermaid$") 31 | endRegex := regexp.MustCompile("^```$") 32 | 33 | // Scanner to read the file line by line 34 | scanner := bufio.NewScanner(file) 35 | 36 | isInMermaidBlock := false 37 | diagramNumber := 1 38 | var diagramContent strings.Builder // Store diagram content temporarily 39 | 40 | for scanner.Scan() { 41 | line := scanner.Text() 42 | 43 | // Check for the start of a mermaid block 44 | if startRegex.MatchString(line) { 45 | isInMermaidBlock = true 46 | diagramContent.Reset() // Clear the previous content 47 | continue 48 | } 49 | 50 | // Check for the end of a mermaid block 51 | if isInMermaidBlock && endRegex.MatchString(line) { 52 | // skip if not requested 53 | if !slices.Contains(ri, fmt.Sprintf("%d", diagramNumber)) { 54 | continue 55 | } 56 | 57 | isInMermaidBlock = false 58 | 59 | // Write the extracted diagram to a .mmd file 60 | outputFile := fmt.Sprintf("diagram_%d.mmd", diagramNumber) 61 | err := os.WriteFile(outputFile, []byte(diagramContent.String()), 0644) 62 | if err != nil { 63 | return fmt.Errorf("failed to write diagram file: %v", err) 64 | } 65 | fmt.Printf("Extracted diagram %d into %s\n", diagramNumber, outputFile) 66 | 67 | diagramNumber++ 68 | continue 69 | } 70 | 71 | // Collect lines for the mermaid block 72 | if isInMermaidBlock { 73 | diagramContent.WriteString(line + "\n") 74 | } 75 | } 76 | 77 | // Check for scanner errors 78 | if err := scanner.Err(); err != nil { 79 | return fmt.Errorf("error reading file: %v", err) 80 | } 81 | 82 | return nil 83 | } -------------------------------------------------------------------------------- /scripts/gen_jsonschema/gen_jsonschema.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/invopop/jsonschema" 10 | 11 | "github.com/pancsta/asyncmachine-go/pkg/integrations" 12 | ) 13 | 14 | func main() { 15 | r := new(jsonschema.Reflector) 16 | if err := r.AddGoComments("github.com/pancsta/asyncmachine-go", 17 | "./pkg/integrations"); err != nil { 18 | panic(err) 19 | } 20 | 21 | // Create docs/jsonschema directory if it doesn't exist 22 | schemaDir := filepath.Join("docs", "jsonschema") 23 | if err := os.MkdirAll(schemaDir, 0755); err != nil { 24 | panic(err) 25 | } 26 | 27 | // List of structs to generate schemas for 28 | structs := []struct { 29 | name string 30 | obj interface{} 31 | }{ 32 | {"msg_kind_req", &integrations.MsgKindReq{}}, 33 | {"msg_kind_resp", &integrations.MsgKindResp{}}, 34 | {"mutation_req", &integrations.MutationReq{}}, 35 | {"mutation_resp", &integrations.MutationResp{}}, 36 | {"waiting_req", &integrations.WaitingReq{}}, 37 | {"waiting_resp", &integrations.WaitingResp{}}, 38 | {"getter_req", &integrations.GetterReq{}}, 39 | {"getter_resp", &integrations.GetterResp{}}, 40 | } 41 | 42 | for _, s := range structs { 43 | schema := r.Reflect(s.obj) 44 | jsonData, err := json.MarshalIndent(schema, "", " ") 45 | if err != nil { 46 | panic(fmt.Errorf("failed to marshal schema for %s: %v", s.name, err)) 47 | } 48 | 49 | filename := filepath.Join(schemaDir, s.name+".json") 50 | if err := os.WriteFile(filename, jsonData, 0644); err != nil { 51 | panic(fmt.Errorf("failed to write schema for %s: %v", s.name, err)) 52 | } 53 | 54 | fmt.Printf("Generated /%s\n", filename) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /scripts/test-loop-record.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | PKG=pkg/rpc 5 | TEST=TestRetryConn 6 | 7 | while true; do 8 | echo "Running tests..." 9 | task clean 10 | 11 | # compile 12 | go test ./$PKG -gcflags 'all=-N -l' -c 13 | # go test ./$PKG -c 14 | 15 | # run & record 16 | env AM_TEST_RUNNER=1 \ 17 | rr record rpc.test -- -test.failfast -test.parallel 1 -test.v -test.run ^${TEST}\$ 18 | # rr record rpc.test -- -test.failfast -test.parallel 1 -test.v 19 | # rr record rpc.test 20 | 21 | # stop 22 | if [ $? -ne 0 ]; then 23 | echo "encountered non-zero exit code: $?"; 24 | exit; 25 | fi 26 | 27 | done -------------------------------------------------------------------------------- /scripts/test-loop-replay.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #set -x 4 | LOCATION=/home/tob/.local/share/rr/rpc.test-2/ 5 | 6 | dlv replay $LOCATION --headless --listen=:2345 --log --api-version=2 7 | -------------------------------------------------------------------------------- /scripts/test-loop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #set -x 4 | 5 | while true; do 6 | echo "Running tests..." 7 | task clean 8 | output=$(env AM_TEST_RUNNER=1 go test ./... -failfast -p 1 -parallel 1 -race -v 2>&1) # Run tests and capture output 9 | 10 | if echo "$output" | grep -q "WARNING: DATA RACE"; then 11 | echo "Race condition detected!" 12 | echo "$output" # Print output for debugging 13 | exit 1 14 | fi 15 | 16 | if echo "$output" | grep -q "FAIL"; then 17 | echo "Failure detected!" 18 | echo "$output" # Print output for debugging 19 | exit 1 20 | fi 21 | 22 | # echo "No race detected. Re-running tests..." 23 | done -------------------------------------------------------------------------------- /tools/cmd/am-dbg/cmd_dbg.go: -------------------------------------------------------------------------------- 1 | // am-dbg is a lightweight, multi-client debugger for asyncmachine-go. 2 | package main 3 | 4 | import ( 5 | "context" 6 | "os" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/pancsta/asyncmachine-go/internal/utils" 11 | "github.com/pancsta/asyncmachine-go/pkg/telemetry" 12 | "github.com/pancsta/asyncmachine-go/tools/debugger" 13 | "github.com/pancsta/asyncmachine-go/tools/debugger/cli" 14 | "github.com/pancsta/asyncmachine-go/tools/debugger/server" 15 | ss "github.com/pancsta/asyncmachine-go/tools/debugger/states" 16 | ) 17 | 18 | func main() { 19 | rootCmd := cli.RootCmd(cliRun) 20 | err := rootCmd.Execute() 21 | if err != nil { 22 | panic(err) 23 | } 24 | } 25 | 26 | // TODO error msgs 27 | func cliRun(_ *cobra.Command, _ []string, p cli.Params) { 28 | ctx := context.Background() 29 | 30 | // print the version 31 | ver := utils.GetVersion() 32 | if p.Version { 33 | println(ver) 34 | os.Exit(0) 35 | } 36 | 37 | // logger and profiler 38 | logger := cli.GetLogger(&p) 39 | cli.StartCpuProfileSrv(ctx, logger, &p) 40 | stopProfile := cli.StartCpuProfile(logger, &p) 41 | if stopProfile != nil { 42 | defer stopProfile() 43 | } 44 | 45 | // init the debugger 46 | dbg, err := debugger.New(ctx, debugger.Opts{ 47 | DbgLogLevel: p.LogLevel, 48 | DbgLogger: logger, 49 | ImportData: p.ImportData, 50 | OutputClients: p.OutputClients, 51 | Diagrams: p.Graph, 52 | Timelines: p.Timelines, 53 | // ...: p.FilterLogLevel, 54 | OutputDir: p.OutputDir, 55 | ServerAddr: p.ListenAddr, 56 | EnableMouse: p.EnableMouse, 57 | SelectConnected: p.SelectConnected, 58 | ShowReader: p.Reader, 59 | CleanOnConnect: p.CleanOnConnect, 60 | MaxMemMb: p.MaxMemMb, 61 | Log2Ttl: p.Log2Ttl, 62 | ViewNarrow: p.ViewNarrow, 63 | ViewRain: p.ViewRain, 64 | TailMode: p.TailMode, 65 | Version: ver, 66 | }) 67 | if err != nil { 68 | panic(err) 69 | } 70 | 71 | // rpc client 72 | if p.DebugAddr != "" { 73 | err := telemetry.TransitionsToDbg(dbg.Mach, p.DebugAddr) 74 | // TODO retries 75 | if err != nil { 76 | panic(err) 77 | } 78 | } 79 | 80 | // rpc server 81 | if p.ListenAddr != "-1" { 82 | go server.StartRpc(dbg.Mach, p.ListenAddr, nil, p.FwdData) 83 | } 84 | 85 | // start and wait till the end 86 | dbg.Start(p.StartupMachine, p.StartupTx, p.StartupView) 87 | 88 | select { 89 | case <-dbg.Mach.WhenDisposed(): 90 | case <-dbg.Mach.WhenNot1(ss.Start, nil): 91 | } 92 | 93 | // show footer stats 94 | printStats(dbg) 95 | 96 | dbg.Dispose() 97 | 98 | // pprof memory profile 99 | cli.HandleProfMem(logger, &p) 100 | } 101 | 102 | func printStats(dbg *debugger.Debugger) { 103 | txs := 0 104 | for _, c := range dbg.Clients { 105 | txs += len(c.MsgTxs) 106 | } 107 | 108 | _, _ = dbg.P.Printf("Clients: %d\n", len(dbg.Clients)) 109 | _, _ = dbg.P.Printf("Transitions: %d\n", txs) 110 | _, _ = dbg.P.Printf("Memory: %dmb\n", debugger.AllocMem()/1024/1024) 111 | } 112 | -------------------------------------------------------------------------------- /tools/cmd/arpc/cmd_arpc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "time" 7 | 8 | "github.com/joho/godotenv" 9 | "github.com/pancsta/asyncmachine-go/internal/utils" 10 | amhelp "github.com/pancsta/asyncmachine-go/pkg/helpers" 11 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 12 | "github.com/pancsta/asyncmachine-go/tools/repl" 13 | "github.com/pancsta/asyncmachine-go/tools/repl/states" 14 | ) 15 | 16 | var ss = states.ReplStates 17 | var randomId = false 18 | 19 | func init() { 20 | _ = godotenv.Load() 21 | // amhelp.EnableDebugging(false) 22 | // amhelp.EnableDebugging(true) 23 | } 24 | 25 | type S = am.S 26 | type T = am.Time 27 | 28 | func main() { 29 | ctx := context.Background() 30 | 31 | var cliArgs []string 32 | var connArgs []string 33 | 34 | osArgs := os.Args[1:] 35 | for i, v := range osArgs { 36 | if v == "--" && len(os.Args) > i+1 { 37 | cliArgs = os.Args[i+2:] 38 | connArgs = os.Args[1:i+1] 39 | break 40 | } 41 | } 42 | 43 | // repl 44 | id := "repl" 45 | if randomId { 46 | id = "repl-" + utils.RandId(4) 47 | } 48 | r, err := repl.New(ctx, id) 49 | if err != nil { 50 | panic(err) 51 | } 52 | rootCmd := repl.NewRootCommand(r, cliArgs, osArgs) 53 | if len(connArgs) > 0 { 54 | rootCmd.SetArgs(connArgs) 55 | } 56 | r.Cmd = rootCmd 57 | 58 | // cobra 59 | err = rootCmd.Execute() 60 | if err != nil { 61 | if amhelp.IsTelemetry() { 62 | time.Sleep(time.Second) 63 | os.Exit(1) 64 | } 65 | } 66 | 67 | // waits 68 | select { 69 | 70 | // CLI 71 | case <-r.Mach.WhenNot1(ss.ReplMode, nil): 72 | // exit 73 | case <-r.Mach.When1(ss.Disposed, nil): 74 | } 75 | 76 | // fmt.Println("bye") 77 | r.Mach.Dispose() 78 | 79 | if amhelp.IsTelemetry() { 80 | time.Sleep(time.Second) 81 | } 82 | 83 | os.Exit(0) 84 | } 85 | -------------------------------------------------------------------------------- /tools/debugger/testdata/am-dbg-sim.gob.br: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pancsta/asyncmachine-go/5ff7b38b3844d5b4de034bb338568eedfe24dd5b/tools/debugger/testdata/am-dbg-sim.gob.br -------------------------------------------------------------------------------- /tools/debugger/utils_test.go: -------------------------------------------------------------------------------- 1 | package debugger 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestHumanSort(t *testing.T) { 11 | data := []string{ 12 | "_nco-dev-test-210946-00e4ff1", 13 | "_rs-nco-dev-test-210946-00e4ff1", 14 | "_rc-con-dev-test-2870ef", 15 | "_rs-nw-loc-dev-test-6791e5", 16 | "_rc-con-dev-test-6791e5", 17 | "_ns-dev-test-210946-0", 18 | "_rs-ns-pub-dev-test-210946-0", 19 | "_rs-ns-loc-dev-test-210946-0", 20 | "_rs-nco-dev-test-210946-0e7608f", 21 | "_rc-ns-dev-test-p-36317", 22 | "_rs-nw-loc-dev-test-b55a1b", 23 | "_nco-dev-test-210946-0c2bbae", 24 | "_rc-ns-dev-test-p-44331", 25 | "_rs-nw-loc-dev-test-2870ef", 26 | "_nw-dev-test-2870ef", 27 | "_nw-dev-test-6791e5", 28 | "_nco-dev-test-210946-0e7608f", 29 | "_nw-dev-test-b55a1b", 30 | "_rc-con-dev-test-b55a1b", 31 | "_rs-nco-dev-test-210946-0c2bbae", 32 | } 33 | 34 | expected := []string{ 35 | "_nco-dev-test-210946-00e4ff1", 36 | "_nco-dev-test-210946-0c2bbae", 37 | "_nco-dev-test-210946-0e7608f", 38 | 39 | "_ns-dev-test-210946-0", 40 | 41 | "_nw-dev-test-2870ef", 42 | "_nw-dev-test-6791e5", 43 | "_nw-dev-test-b55a1b", 44 | 45 | "_rc-con-dev-test-2870ef", 46 | "_rc-con-dev-test-6791e5", 47 | "_rc-con-dev-test-b55a1b", 48 | "_rc-ns-dev-test-p-36317", 49 | "_rc-ns-dev-test-p-44331", 50 | "_rs-nco-dev-test-210946-00e4ff1", 51 | "_rs-nco-dev-test-210946-0c2bbae", 52 | "_rs-nco-dev-test-210946-0e7608f", 53 | "_rs-ns-loc-dev-test-210946-0", 54 | "_rs-ns-pub-dev-test-210946-0", 55 | "_rs-nw-loc-dev-test-2870ef", 56 | "_rs-nw-loc-dev-test-6791e5", 57 | "_rs-nw-loc-dev-test-b55a1b", 58 | } 59 | 60 | humanSort(data) 61 | join := strings.Join(data, "\n") 62 | 63 | // debug 64 | // t.Log("\n" + join) 65 | 66 | assert.Equal(t, strings.Join(expected, "\n"), join) 67 | } 68 | 69 | func TestHadErrSince(t *testing.T) { 70 | c := &Client{ 71 | errors: []int{100, 50, 5, 1}, 72 | } 73 | 74 | assert.False(t, c.hadErrSinceTx(300, 5), "tx: %d, dist: %d", 300, 5) 75 | assert.False(t, c.hadErrSinceTx(105, 3), "tx: %d, dist: %d", 105, 3) 76 | 77 | assert.True(t, c.hadErrSinceTx(55, 10), "tx: %d, dist: %d", 55, 10) 78 | assert.True(t, c.hadErrSinceTx(6, 2), "tx: %d, dist: %d", 6, 2) 79 | assert.True(t, c.hadErrSinceTx(100, 2), "tx: %d, dist: %d", 100, 2) 80 | assert.True(t, c.hadErrSinceTx(1, 1), "tx: %d, dist: %d", 1, 1) 81 | } 82 | -------------------------------------------------------------------------------- /tools/generator/states/ss_generator.go: -------------------------------------------------------------------------------- 1 | package states 2 | 3 | import ( 4 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 5 | . "github.com/pancsta/asyncmachine-go/pkg/states/global" 6 | ) 7 | 8 | // GeneratorStatesDef contains all the states of the Client state machine. 9 | type GeneratorStatesDef struct { 10 | *am.StatesBase 11 | 12 | // pkg/states 13 | InheritBasic string 14 | InheritConnected string 15 | InheritDisposed string 16 | 17 | // pkg/* 18 | InheritRpcWorker string 19 | InheritNodeWorker string 20 | 21 | // rest 22 | Inherit string 23 | GroupsLocal string 24 | GroupsInherited string 25 | Groups string 26 | } 27 | 28 | // GeneratorGroupsDef contains all the state groups %s state machine. 29 | type GeneratorGroupsDef struct { 30 | Inherit S 31 | } 32 | 33 | // GeneratorSchema represents all relations and properties of GeneratorStates. 34 | var GeneratorSchema = am.Schema{ 35 | ssG.InheritBasic: {}, 36 | ssG.InheritConnected: {Add: S{ssG.GroupsInherited}}, 37 | ssG.InheritDisposed: {}, 38 | 39 | ssG.InheritRpcWorker: {}, 40 | ssG.InheritNodeWorker: { 41 | Add: S{ssG.GroupsInherited}, 42 | Remove: S{ssG.InheritRpcWorker}, 43 | }, 44 | 45 | ssG.Inherit: {Auto: true}, 46 | ssG.GroupsLocal: {}, 47 | ssG.GroupsInherited: {}, 48 | ssG.Groups: {Auto: true}, 49 | } 50 | 51 | // EXPORTS AND GROUPS 52 | 53 | var ( 54 | ssG = am.NewStates(GeneratorStatesDef{}) 55 | sgG = am.NewStateGroups(GeneratorGroupsDef{ 56 | Inherit: S{ssG.InheritBasic, ssG.InheritConnected, ssG.InheritRpcWorker, 57 | ssG.InheritNodeWorker, ssG.InheritDisposed}, 58 | }) 59 | 60 | // GeneratorStates contains all the states for the Generator machine. 61 | GeneratorStates = ssG 62 | // GeneratorGroups contains all the state groups for the Generator machine. 63 | GeneratorGroups = sgG 64 | ) 65 | -------------------------------------------------------------------------------- /tools/repl/states/ss_repl.go: -------------------------------------------------------------------------------- 1 | // Package states contains a stateful schema-v2 for Repl. 2 | // Bootstrapped with am-gen. Edit manually or re-gen & merge. 3 | package states 4 | 5 | import ( 6 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 7 | ss "github.com/pancsta/asyncmachine-go/pkg/states" 8 | . "github.com/pancsta/asyncmachine-go/pkg/states/global" 9 | ) 10 | 11 | // ReplStatesDef contains all the states of the Repl state machine. 12 | type ReplStatesDef struct { 13 | *am.StatesBase 14 | 15 | ErrSyntax string 16 | 17 | // CONNECTION 18 | 19 | Disconnected string 20 | Connecting string 21 | Connected string 22 | ConnectedFully string 23 | Disconnecting string 24 | 25 | // PIPES 26 | 27 | RpcConn string 28 | RpcDisconn string 29 | 30 | // REPL CMDS 31 | 32 | CmdAdd string 33 | CmdRemove string 34 | CmdGroupAdd string 35 | CmdGroupRemove string 36 | CmdList string 37 | CmdScript string 38 | CmdWhenTime string 39 | CmdWhen string 40 | CmdWhenNot string 41 | CmdInspect string 42 | CmdStatus string 43 | 44 | // REST 45 | 46 | // REPL is running in a TUI mode 47 | ReplMode string 48 | // List fully connected machines, with filters. 49 | ListMachines string 50 | 51 | // inherit from BasicStatesDef 52 | *ss.BasicStatesDef 53 | // inherit from ConnPoolStatesDef 54 | *ss.ConnPoolStatesDef 55 | // inherit from DisposedStatesDef 56 | *ss.DisposedStatesDef 57 | } 58 | 59 | // ReplGroupsDef contains all the state groups Repl state machine. 60 | type ReplGroupsDef struct { 61 | *ss.ConnectedGroupsDef 62 | 63 | Cmds S 64 | } 65 | 66 | // ReplSchema represents all relations and properties of ReplStates. 67 | var ReplSchema = SchemaMerge( 68 | // inherit from BasicStruct 69 | ss.BasicSchema, 70 | // inherit from ConnPoolSchema 71 | ss.ConnPoolSchema, 72 | // inherit from DisposedStruct 73 | ss.DisposedSchema, 74 | am.Schema{ 75 | 76 | ssC.ErrSyntax: {}, 77 | 78 | // PIPES 79 | 80 | ssC.RpcConn: {Multi: true}, 81 | ssC.RpcDisconn: {Multi: true}, 82 | 83 | // CMDS 84 | 85 | ssC.CmdAdd: { 86 | Multi: true, 87 | Require: S{ssC.Connected}, 88 | }, 89 | ssC.CmdRemove: { 90 | Multi: true, 91 | Require: S{ssC.Connected}, 92 | }, 93 | ssC.CmdGroupAdd: { 94 | Multi: true, 95 | Require: S{ssC.Connected}, 96 | }, 97 | ssC.CmdGroupRemove: { 98 | Multi: true, 99 | Require: S{ssC.Connected}, 100 | }, 101 | ssC.CmdList: { 102 | Multi: true, 103 | Require: S{ssC.Connected}, 104 | }, 105 | ssC.CmdScript: { 106 | Multi: true, 107 | Require: S{ssC.Connected}, 108 | }, 109 | ssC.CmdWhenTime: { 110 | Multi: true, 111 | Require: S{ssC.Connected}, 112 | }, 113 | ssC.CmdWhen: { 114 | Multi: true, 115 | Require: S{ssC.Connected}, 116 | }, 117 | ssC.CmdWhenNot: { 118 | Multi: true, 119 | Require: S{ssC.Connected}, 120 | }, 121 | ssC.CmdInspect: { 122 | Multi: true, 123 | Require: S{ssC.Connected}, 124 | }, 125 | ssC.CmdStatus: { 126 | Multi: true, 127 | Require: S{ssC.Connected}, 128 | }, 129 | 130 | // STATUS 131 | 132 | ssC.ReplMode: {Require: S{ssC.Start}}, 133 | 134 | // ACTIONS 135 | 136 | ssC.ListMachines: { 137 | Multi: true, 138 | Require: S{ssC.Start}, 139 | }, 140 | }) 141 | 142 | // EXPORTS AND GROUPS 143 | 144 | var ( 145 | ssC = am.NewStates(ReplStatesDef{}) 146 | sgC = am.NewStateGroups(ReplGroupsDef{ 147 | Cmds: S{ssC.CmdAdd, ssC.CmdRemove, ssC.CmdList, ssC.CmdScript, 148 | ssC.CmdWhenTime, ssC.CmdWhen, ssC.CmdWhenNot, ssC.CmdInspect, 149 | ssC.CmdStatus}, 150 | }, ss.ConnectedGroups) 151 | 152 | // ReplStates contains all the states for the Repl machine. 153 | ReplStates = ssC 154 | // ReplGroups contains all the state groups for the Repl machine. 155 | ReplGroups = sgC 156 | ) 157 | --------------------------------------------------------------------------------