├── .github ├── BUMP └── workflows │ ├── releaser.yml │ ├── go-basic.yml │ ├── linter.yml │ ├── go-tools.yml │ ├── go-node.yml │ ├── go-rpc.yml │ ├── go-pubsub.yml │ ├── go-states.yml │ ├── go-helpers.yml │ ├── go-history.yml │ ├── go-machine.yml │ └── go-telemetry.yml ├── config ├── env │ ├── debug-simple.env │ ├── debug-pubsub.env │ ├── debug-node.env │ ├── debug-telemetry-dbg.env │ ├── tests-remote.env │ ├── debug-tests.env │ └── debug-rpc.env ├── .mdlrc ├── cli │ └── debug-am-dbg.txt ├── .mdl_style.rb ├── dashboards │ ├── dash-repl.kdl │ ├── dash-repl-web.kdl │ ├── dash-wide-narrow.kdl │ ├── dash-wide-double-narrow.kdl │ ├── dash-full.kdl │ └── dash-wide-narrow-repl.kdl └── terminalizer.yml ├── examples ├── benchmark_state_source │ ├── bench │ │ ├── .gitignore │ │ ├── Dockerfile │ │ ├── Caddyfile-root │ │ ├── Caddyfile-root-2 │ │ ├── Caddyfile-root-2-4 │ │ ├── Caddyfile-root-2-6 │ │ └── go-wrk.sh │ ├── Dockerfile │ └── Taskfile.yml ├── arpc │ ├── Procfile │ ├── Taskfile.yml │ ├── states │ │ ├── states_utils.go │ │ └── ss_example.go │ ├── client │ │ └── main.go │ └── server │ │ └── main.go ├── .editorconfig ├── tree_state_source │ ├── Procfile │ ├── config.env │ ├── states │ │ └── states_utils.go │ ├── gen-grafana.sh │ ├── gen_states │ │ └── gen_states.go │ ├── Taskfile.yml │ └── README.md ├── mach_template │ ├── gen-states.sh │ ├── example.env │ └── states │ │ ├── states_utils.go │ │ └── ss_mach_template.go ├── relations_playground │ └── relations_playground_test.go ├── repl │ ├── states │ │ ├── states_utils.go │ │ └── ss_example.go │ └── main.go ├── benchmark_grpc │ ├── proto │ │ └── worker.proto │ ├── states │ │ ├── states_utils.go │ │ └── ss_worker.go │ ├── worker_proto │ │ └── worker.proto │ ├── client_local_test.go │ ├── worker_states │ │ └── worker_states.go │ ├── worker.go │ ├── server_grpc.go │ └── server_arpc.go ├── asynq_fileprocessing │ ├── client │ │ └── client.go │ └── worker │ │ └── worker.go ├── nfa │ └── states │ │ ├── states_utils.go │ │ └── ss_nfa.go ├── temporal_fileprocessing │ ├── fileprocessing_test.go │ └── states │ │ └── ss_fileprocessing.go ├── temporal_expense │ └── states │ │ └── ss_expense.go ├── path_watcher │ └── states │ │ └── ss_watcher.go ├── pipes │ └── example_pipes.go ├── fan_out_in │ └── example_fan_out_in.go ├── subscriptions │ └── example_subscriptions.go ├── raw_strings │ └── raw_strings.go └── fsm │ └── fsm_test.go ├── pkg ├── history │ ├── hist_example_test.go │ ├── test │ │ ├── history_test.go │ │ └── test_hist.go │ └── bbolt │ │ └── bbolt_test.go ├── states │ ├── states_utils.go │ ├── global │ │ └── states_utils.go │ ├── ss_basic.go │ └── ss_disposed.go ├── rpc │ ├── utils_test.go │ └── states │ │ ├── ss_rpc_consumer.go │ │ ├── ss_rpc_worker.go │ │ ├── ss_mux.go │ │ ├── ss_rpc_shared.go │ │ ├── ss_rpc_server.go │ │ └── ss_rpc_client.go ├── helpers │ └── help_test.go ├── telemetry │ ├── otel_test.go │ └── telemetry.go ├── node │ ├── states │ │ ├── ss_bootstrap.go │ │ └── ss_node_client.go │ └── test │ │ └── worker │ │ └── node_test_worker.go ├── x │ ├── history │ │ └── frostdb │ │ │ └── frostdb_test.go │ └── helpers │ │ └── x_help_test.go └── integrations │ └── README.md ├── .gitmodules ├── tools ├── debugger │ ├── testdata │ │ └── am-dbg-sim.gob.br │ ├── utils_test.go │ └── types │ │ └── dbg_types.go ├── visualizer │ ├── visualizer_test.go │ └── states │ │ └── ss_visualizer.go ├── cmd │ ├── am-relay │ │ ├── cmd_relay.go │ │ └── README.md │ └── arpc │ │ └── cmd_arpc.go ├── relay │ ├── types │ │ └── relay_cli.go │ └── states │ │ └── ss_relay.go └── generator │ └── states │ └── ss_generator.go ├── scripts ├── test_loop │ ├── test-loop-replay.sh │ ├── test-loop-record.sh │ └── test-loop.sh ├── dep-taskfile.sh ├── compact_number │ └── compact_number.go ├── gen_jsonschema │ └── gen_jsonschema.go ├── gen_website │ └── sitemap │ │ └── sitemap.go └── extract_mermaid │ └── extract_mermaid.go ├── deploy ├── web-metrics │ ├── prometheus.yaml │ ├── otel-collector-config.yaml │ ├── loki.yaml │ └── docker-compose.yml └── web-am-dbg │ └── Dockerfile ├── .editorconfig ├── .github_changelog_generator ├── ROADMAP.md ├── .dockerignore ├── .golangci.yml ├── docs ├── jsonschema │ ├── msg_kind_req.json │ ├── msg_kind_resp.json │ ├── mutation_resp.json │ ├── mutation_req.json │ ├── waiting_req.json │ ├── waiting_resp.json │ ├── getter_req.json │ └── getter_resp.json └── diagrams-examples.md ├── LICENSE ├── internal ├── testing │ ├── states │ │ └── ss_rel.go │ └── cmd │ │ └── am-dbg-worker │ │ └── main_dbg_worker.go └── utils │ └── utils.go ├── .gitignore ├── codecov.yaml └── .goreleaser.yml /.github/BUMP: -------------------------------------------------------------------------------- 1 | 31 -------------------------------------------------------------------------------- /config/env/debug-simple.env: -------------------------------------------------------------------------------- 1 | AM_DEBUG=1 2 | -------------------------------------------------------------------------------- /examples/benchmark_state_source/bench/.gitignore: -------------------------------------------------------------------------------- 1 | Caddyfile -------------------------------------------------------------------------------- /config/.mdlrc: -------------------------------------------------------------------------------- 1 | style "#{File.dirname(__FILE__)}/.mdl_style.rb" -------------------------------------------------------------------------------- /config/env/debug-pubsub.env: -------------------------------------------------------------------------------- 1 | AM_PUBSUB_LOG=1 2 | AM_PUBSUB_DBG=1 3 | -------------------------------------------------------------------------------- /config/cli/debug-am-dbg.txt: -------------------------------------------------------------------------------- 1 | --am-dbg-addr=localhost:9913 2 | --log-level=2 -------------------------------------------------------------------------------- /examples/arpc/Procfile: -------------------------------------------------------------------------------- 1 | client: task client 2 | server: task server 3 | -------------------------------------------------------------------------------- /config/env/debug-node.env: -------------------------------------------------------------------------------- 1 | AM_NODE_LOG_SUPERVISOR=1 2 | AM_NODE_LOG_CLIENT=1 3 | -------------------------------------------------------------------------------- /examples/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.go] 2 | indent_size = 4 3 | max_line_length = 120 4 | -------------------------------------------------------------------------------- /pkg/history/hist_example_test.go: -------------------------------------------------------------------------------- 1 | package history 2 | 3 | // TODO ExampleTrack 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "assets"] 2 | path = assets 3 | url = https://github.com/pancsta/assets.git 4 | -------------------------------------------------------------------------------- /config/env/debug-telemetry-dbg.env: -------------------------------------------------------------------------------- 1 | AM_DBG_ADDR=localhost:6831 2 | AM_LOG=3 3 | AM_LOG_FULL=1 4 | AM_HEALTHCHECK=1 5 | -------------------------------------------------------------------------------- /config/env/tests-remote.env: -------------------------------------------------------------------------------- 1 | AM_DBG_WORKER_RPC_ADDR=localhost:53480 2 | AM_DBG_WORKER_TELEMETRY_ADDR=localhost:53470 3 | -------------------------------------------------------------------------------- /config/env/debug-tests.env: -------------------------------------------------------------------------------- 1 | AM_DEBUG=1 2 | AM_DBG_ADDR=1 3 | AM_LOG=3 4 | AM_LOG_FULL=1 5 | AM_TEST_DEBUG=1 6 | AM_DETECT_EVAL=1 -------------------------------------------------------------------------------- /tools/debugger/testdata/am-dbg-sim.gob.br: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pancsta/asyncmachine-go/HEAD/tools/debugger/testdata/am-dbg-sim.gob.br -------------------------------------------------------------------------------- /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_DIR=tmp 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /scripts/test_loop/test-loop-replay.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #set -x 4 | LOCATION=~/.local/share/rr/rpc.test-2/ 5 | 6 | dlv replay $LOCATION --headless --listen=:2345 --log --api-version=2 7 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /.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_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 -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /scripts/dep-taskfile.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # Roadmap 2 | 3 | - history layer - SQL, KV, ... 4 | - am-relay tool for proxying, conversions, duck-taping 5 | - RPC handlers for distributed state-machines 6 | - scheduler via the handler loop 7 | - go1.22 traces 8 | - inference 9 | - optimizations 10 | - tutorials 11 | 12 | See also [issues](https://github.com/pancsta/asyncmachine-go/issues). 13 | -------------------------------------------------------------------------------- /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/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=3 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 | -------------------------------------------------------------------------------- /.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 | tmp 17 | 18 | # Ignore Dockerfile and docker-compose.yml 19 | Dockerfile 20 | docker-compose.yml 21 | 22 | # Ignore Go build artifacts 23 | *.exe 24 | *.exe~ 25 | *.dll 26 | *.so 27 | *.dylib 28 | *.test 29 | *.out 30 | 31 | # Ignore IDE/editor specific files 32 | .vscode 33 | .idea 34 | *.swp 35 | *~ 36 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /scripts/test_loop/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 -------------------------------------------------------------------------------- /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_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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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] -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 }} -------------------------------------------------------------------------------- /scripts/test_loop/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 -------------------------------------------------------------------------------- /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=2 17 | AM_LOG=3 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_DIR=tmp 34 | # go test debug 35 | # -gcflags="-N -l" -v 36 | -------------------------------------------------------------------------------- /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.StateException 26 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | enable: 4 | - lll 5 | settings: 6 | lll: 7 | line-length: 80 8 | tab-width: 2 9 | staticcheck: 10 | dot-import-whitelist: 11 | - "github.com/pancsta/asyncmachine-go/pkg/states/global" 12 | checks: 13 | - '-SA1000' # err comments 14 | exclusions: 15 | generated: lax 16 | presets: 17 | - comments 18 | - common-false-positives 19 | - legacy 20 | - std-error-handling 21 | paths: 22 | - vendor 23 | - third_party 24 | - pkg/pubsub/uds 25 | - third_party$ 26 | - builtin$ 27 | - ^examples 28 | - ^scripts 29 | formatters: 30 | exclusions: 31 | generated: lax 32 | paths: 33 | - third_party$ 34 | - builtin$ 35 | - ^examples 36 | - ^scripts 37 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.StateException 26 | -------------------------------------------------------------------------------- /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.StateException 26 | -------------------------------------------------------------------------------- /tools/visualizer/visualizer_test.go: -------------------------------------------------------------------------------- 1 | package visualizer 2 | 3 | // func TestUpdates(t *testing.T) { 4 | // sel := &Fragment{ 5 | // MachId: "cook", 6 | // States: am.S{"Exception", "StoryCookingStarted", "StoryJoke", "Ready", 7 | // "Requesting", "StoryStartAgain", "Start", "BaseDBStarting", 8 | // "CheckStories"}, 9 | // // Active: am.S{"StoryJoke"}, 10 | // Active: am.S{"Ready", "StoryStartAgain", "Start", "CheckStories"}, 11 | // } 12 | // 13 | // path := "testdata/sample.svg" 14 | // file, err := os.Open(path) 15 | // if err != nil { 16 | // t.Fatal(err) 17 | // } 18 | // defer file.Close() 19 | // 20 | // dom, err := goquery.NewDocumentFromReader(file) 21 | // if err != nil { 22 | // t.Fatal(err) 23 | // } 24 | // 25 | // err = UpdateCache(context.Background(), path, dom, sel) 26 | // if err != nil { 27 | // t.Fatal(err) 28 | // } 29 | // } 30 | -------------------------------------------------------------------------------- /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 | // TODO 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(nil), 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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /deploy/web-am-dbg/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM archlinux:latest 2 | 3 | # OS 4 | 5 | RUN pacman -Sy --noconfirm go git ca-certificates \ 6 | ttyd go-task zellij xclip xsel 7 | RUN mkdir -p /app 8 | RUN cp /usr/bin/go-task /usr/bin/task 9 | RUN mkdir -p ~/.config/zellij/ 10 | RUN echo "show_startup_tips false" > ~/.config/zellij/config.kdl 11 | RUN echo "show_release_notes false" >> ~/.config/zellij/config.kdl 12 | 13 | # DEPS 14 | 15 | COPY ./cview /app/cview 16 | COPY ./asyncmachine-go/go.mod /app/asyncmachine-go/ 17 | COPY ./asyncmachine-go/go.sum /app/asyncmachine-go/ 18 | 19 | # PREBUILD 20 | 21 | WORKDIR /app/asyncmachine-go 22 | RUN --mount=type=cache,target=/root/go/pkg/mod \ 23 | go mod download 24 | 25 | # SOURCE 26 | 27 | COPY ./asyncmachine-go /app/asyncmachine-go 28 | RUN --mount=type=cache,target=/root/.cache/go-build \ 29 | task build-am-dbg 30 | RUN --mount=type=cache,target=/root/.cache/go-build \ 31 | task build-arpc 32 | 33 | # START 34 | 35 | # ports: am-dbg ttyd 36 | EXPOSE 6831 7681 37 | ENTRYPOINT ["go-task", "web-dashboard-repl"] 38 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /pkg/helpers/help_test.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | type Args 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 := &Args{ 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 := &Args{ 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, 0) 50 | 51 | assert.Equal(t, expected["log1"], logMap["log1"]) 52 | assert.Len(t, logMap, 1) 53 | } 54 | -------------------------------------------------------------------------------- /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.StateException 32 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.SemLogger().SetLevel(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, "[auto_] +A") 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024-present pancsta 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. -------------------------------------------------------------------------------- /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.StateException, 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/dashboards/dash-repl.kdl: -------------------------------------------------------------------------------- 1 | keybinds clear-defaults=true { 2 | locked { 3 | // navi 4 | bind "Alt Left" { 5 | MoveFocusOrTab "Left"; } 6 | bind "Alt Right" { 7 | MoveFocusOrTab "Right"; } 8 | bind "Alt Down" { 9 | MoveFocus "Down"; } 10 | bind "Alt Up" { 11 | MoveFocus "Up"; } 12 | bind "Alt =" { 13 | Resize "Increase"; } 14 | 15 | // resize 16 | bind "Alt =" "Alt +" { 17 | Resize "Increase"; } 18 | bind "Alt -" { 19 | Resize "Decrease"; } 20 | 21 | 22 | bind "Ctrl q" { 23 | Quit ; } 24 | } 25 | } 26 | 27 | pane_frames false 28 | copy_on_select false 29 | pane_frames false 30 | ui { 31 | pane_frames { 32 | hide_session_name true 33 | rounded_corners true 34 | } 35 | } 36 | show_release_notes false 37 | show_startup_tips false 38 | 39 | layout { 40 | pane split_direction="horizontal" { 41 | pane size="85%" command="sh" { 42 | args "-c" "./dist/am-dbg --dir $AM_REPL_DIR -l $AM_DBG_ADDR --tail --output-clients --output-tx --max-mem 1000" 43 | } 44 | pane size="15%" command="sh" { 45 | args "-c" "sleep 5 && ./dist/arpc -d $AM_REPL_DIR --watch" 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /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.SetEnvLogLevel(am.LogOps) 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 | -------------------------------------------------------------------------------- /config/dashboards/dash-repl-web.kdl: -------------------------------------------------------------------------------- 1 | keybinds clear-defaults=true { 2 | locked { 3 | // navi 4 | bind "Alt Left" { 5 | MoveFocusOrTab "Left"; } 6 | bind "Alt Right" { 7 | MoveFocusOrTab "Right"; } 8 | bind "Alt Down" { 9 | MoveFocus "Down"; } 10 | bind "Alt Up" { 11 | MoveFocus "Up"; } 12 | bind "Alt =" { 13 | Resize "Increase"; } 14 | 15 | // resize 16 | bind "Alt =" "Alt +" { 17 | Resize "Increase"; } 18 | bind "Alt -" { 19 | Resize "Decrease"; } 20 | 21 | 22 | bind "Ctrl q" { 23 | Quit ; } 24 | } 25 | } 26 | 27 | pane_frames false 28 | copy_on_select false 29 | pane_frames false 30 | ui { 31 | pane_frames { 32 | hide_session_name true 33 | rounded_corners true 34 | } 35 | } 36 | show_release_notes false 37 | show_startup_tips false 38 | 39 | layout { 40 | pane split_direction="horizontal" { 41 | pane size="85%" command="sh" { 42 | args "-c" "./dist/am-dbg --dir $AM_REPL_DIR -l $AM_DBG_ADDR --tail --output-clients --output-tx --max-mem 1000 --enable-clipboard=false" 43 | } 44 | pane size="15%" command="sh" { 45 | args "-c" "sleep 5 && ./dist/arpc -d $AM_REPL_DIR --watch" 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tools/cmd/am-relay/cmd_relay.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/alexflint/go-arg" 8 | 9 | "github.com/pancsta/asyncmachine-go/tools/relay" 10 | "github.com/pancsta/asyncmachine-go/tools/relay/states" 11 | "github.com/pancsta/asyncmachine-go/tools/relay/types" 12 | ) 13 | 14 | var ss = states.RelayStates 15 | var args types.Args 16 | 17 | func main() { 18 | p := arg.MustParse(&args) 19 | ctx := context.Background() 20 | if p.Subcommand() == nil { 21 | p.Fail("missing subcommand (" + types.CmdRotateDbg + ")") 22 | } 23 | 24 | switch { 25 | case args.RotateDbg != nil: 26 | if err := rotateDbg(ctx, &args); err != nil { 27 | _ = p.FailSubcommand(err.Error(), types.CmdRotateDbg) 28 | } 29 | } 30 | 31 | } 32 | 33 | func rotateDbg(ctx context.Context, args *types.Args) error { 34 | r, err := relay.New(ctx, args, func(msg string, args ...any) { 35 | fmt.Printf(msg, args...) 36 | }) 37 | if err != nil { 38 | return err 39 | } 40 | r.Mach.Add1(ss.Start, nil) 41 | <-r.Mach.WhenNot1(ss.Start, nil) 42 | if r.Mach.Is1(ss.Exception) { 43 | fmt.Printf("last err: %v\n", r.Mach.Err()) 44 | } 45 | fmt.Println("exiting am-relay, bye") 46 | 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /config/dashboards/dash-wide-narrow.kdl: -------------------------------------------------------------------------------- 1 | keybinds clear-defaults=true { 2 | locked { 3 | // navi 4 | bind "Alt Left" { 5 | MoveFocusOrTab "Left"; } 6 | bind "Alt Right" { 7 | MoveFocusOrTab "Right"; } 8 | bind "Alt Down" { 9 | MoveFocus "Down"; } 10 | bind "Alt Up" { 11 | MoveFocus "Up"; } 12 | bind "Alt =" { 13 | Resize "Increase"; } 14 | 15 | // resize 16 | bind "Alt =" "Alt +" { 17 | Resize "Increase"; } 18 | bind "Alt -" { 19 | Resize "Decrease"; } 20 | 21 | 22 | bind "Ctrl q" { 23 | Quit ; } 24 | } 25 | } 26 | 27 | pane_frames false 28 | copy_on_select false 29 | pane_frames false 30 | ui { 31 | pane_frames { 32 | hide_session_name true 33 | rounded_corners true 34 | } 35 | } 36 | show_release_notes false 37 | show_startup_tips false 38 | 39 | layout { 40 | pane split_direction="vertical" { 41 | pane size="75%" command="sh" { 42 | args "-c" "sleep 2 && task am-dbg -- --dir tmp -l localhost:6831 --fwd-data localhost:6832 --output-diagrams 3 --tail --output-clients" 43 | } 44 | pane size="25%" command="sh" { 45 | args "-c" "task am-dbg -- -l localhost:6832 --view-narrow --view-timelines 0 --tail --view-timelines 1" 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /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 | ssam "github.com/pancsta/asyncmachine-go/pkg/states" 7 | . "github.com/pancsta/asyncmachine-go/pkg/states/global" 8 | ) 9 | 10 | // BootstrapStatesDef contains all the states of the Bootstrap state machine. 11 | // The target state is WorkerAddr, activated by an aRPC client. 12 | type BootstrapStatesDef struct { 13 | *am.StatesBase 14 | 15 | // WorkerAddr - The awaited worker passed its connection details. 16 | WorkerAddr string 17 | 18 | // inherit from BasicStatesDef 19 | *ssam.BasicStatesDef 20 | // inherit from WorkerStatesDef 21 | *ssrpc.WorkerStatesDef 22 | } 23 | 24 | // BootstrapSchema represents all relations and properties of 25 | // BootstrapStatesDef. 26 | var BootstrapSchema = SchemaMerge( 27 | // inherit from BasicSchema 28 | ssam.BasicSchema, 29 | // inherit from WorkerStruct 30 | ssrpc.WorkerSchema, 31 | am.Schema{ 32 | cos.WorkerAddr: {}, 33 | }) 34 | 35 | // EXPORTS AND GROUPS 36 | 37 | var ( 38 | cos = am.NewStates(BootstrapStatesDef{}) 39 | 40 | // BootstrapStates contains all the states for the Bootstrap machine. 41 | BootstrapStates = cos 42 | ) 43 | -------------------------------------------------------------------------------- /tools/relay/types/relay_cli.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "time" 4 | 5 | const CmdRotateDbg = "rotate-dbg" 6 | 7 | type OutputFunc func(msg string, args ...any) 8 | 9 | // nolint:lll 10 | type Args struct { 11 | RotateDbg *ArgsRotateDbg `arg:"subcommand:rotate-dbg" help:"Rotate dbg protocol with fragmented dump files"` 12 | Debug bool `arg:"--debug" help:"Enable debugging for asyncmachine"` 13 | // TODO ID suffix 14 | } 15 | 16 | // ArgsRotateDbg converts dbg dumps to other formats / versions. 17 | // am-relay convert-dbg am-dbg-dump.gob.br -o am-vis 18 | // nolint:lll 19 | type ArgsRotateDbg struct { 20 | ListenAddr string `arg:"-l,--listen-addr" default:"localhost:2732" help:"Listen address for RPC server"` 21 | FwdAddr []string `arg:"-f,--fwd-addr,separate" help:"Address of an RPC server to forward data to (repeatable)"` 22 | IntervalTx int `arg:"--interval-tx" default:"10000" help:"Amount of transitions to create a dump file"` 23 | IntervalTime time.Duration `arg:"--interval-duration" default:"24h" help:"Amount of human time to create a dump file"` 24 | Filename string `arg:"-o,--output" default:"am-dbg-dump" help:"Output file base name"` 25 | Dir string `arg:"-d,--dir" default:"." help:"Output directory"` 26 | } 27 | -------------------------------------------------------------------------------- /.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 | tools/visualizer/testdata/* 78 | *.sqlite 79 | *.sqlite-journal 80 | *.sqlite-shm 81 | *.sqlite-wal 82 | /pkg/history/frostdb/databases/ 83 | /pkg/x/history/frostdb/databases/ 84 | /pkg/history/bbolt/amhist.db 85 | /*.d2 86 | /docs/api 87 | /docs/website 88 | -------------------------------------------------------------------------------- /codecov.yaml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "pkg/pubsub/uds/**" # belongs to libp2p 3 | 4 | component_management: 5 | 6 | default_rules: 7 | statuses: 8 | - type: project 9 | target: auto 10 | branches: 11 | - "!main" 12 | 13 | individual_components: 14 | - component_id: machine 15 | name: pkg/machine 16 | paths: 17 | - pkg/machine/** 18 | - component_id: helpers 19 | name: pkg/helpers 20 | paths: 21 | - pkg/helpers/** 22 | - component_id: history 23 | name: pkg/history 24 | paths: 25 | - pkg/history/** 26 | - component_id: node 27 | name: pkg/node 28 | paths: 29 | - pkg/node/** 30 | - component_id: pubsub 31 | name: pkg/pubsub 32 | paths: 33 | - pkg/pubsub/** 34 | - component_id: rpc 35 | name: pkg/rpc 36 | paths: 37 | - pkg/rpc/** 38 | - component_id: states 39 | name: pkg/states 40 | paths: 41 | - pkg/states/** 42 | - component_id: telemetry 43 | name: pkg/telemetry 44 | paths: 45 | - pkg/telemetry/** 46 | 47 | - component_id: tools 48 | name: tools 49 | paths: 50 | - tools/** 51 | 52 | - component_id: internal 53 | name: internal 54 | paths: 55 | - internal/** -------------------------------------------------------------------------------- /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/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/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.StateException, 56 | Start, 57 | Event, 58 | Value1, 59 | Value2, 60 | Value3, 61 | CallOp, 62 | } 63 | 64 | // #endregion 65 | -------------------------------------------------------------------------------- /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/global" 6 | ) 7 | 8 | // WorkerStatesDef contains all the states of the Worker state machine. 9 | type WorkerStatesDef struct { 10 | *am.StatesBase 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 the requested payload to the RPC server 23 | // using rpc.Pass, rpc.A, and rpc.ArgsPayload. 24 | SendPayload string 25 | } 26 | 27 | // WorkerSchema represents all relations and properties of WorkerStates. 28 | var WorkerSchema = SchemaMerge( 29 | am.Schema{ 30 | 31 | // errors 32 | 33 | ssW.ErrProviding: {Require: S{am.StateException}}, 34 | ssW.ErrSendPayload: {Require: S{am.StateException}}, 35 | 36 | // rcp getter 37 | 38 | ssW.SendPayload: {Multi: true}, 39 | }) 40 | 41 | // EXPORTS AND GROUPS 42 | 43 | var ( 44 | // ssW is worker states from WorkerStatesDef. 45 | ssW = am.NewStates(WorkerStatesDef{}) 46 | 47 | // WorkerStates contains all the states for the Worker machine. 48 | WorkerStates = ssW 49 | ) 50 | -------------------------------------------------------------------------------- /config/dashboards/dash-wide-double-narrow.kdl: -------------------------------------------------------------------------------- 1 | keybinds clear-defaults=true { 2 | locked { 3 | // navi 4 | bind "Alt Left" { 5 | MoveFocusOrTab "Left"; } 6 | bind "Alt Right" { 7 | MoveFocusOrTab "Right"; } 8 | bind "Alt Down" { 9 | MoveFocus "Down"; } 10 | bind "Alt Up" { 11 | MoveFocus "Up"; } 12 | bind "Alt =" { 13 | Resize "Increase"; } 14 | 15 | // resize 16 | bind "Alt =" "Alt +" { 17 | Resize "Increase"; } 18 | bind "Alt -" { 19 | Resize "Decrease"; } 20 | 21 | 22 | bind "Ctrl q" { 23 | Quit ; } 24 | } 25 | } 26 | 27 | pane_frames false 28 | copy_on_select false 29 | pane_frames false 30 | ui { 31 | pane_frames { 32 | hide_session_name true 33 | rounded_corners true 34 | } 35 | } 36 | show_release_notes false 37 | show_startup_tips false 38 | 39 | layout { 40 | pane split_direction="vertical" { 41 | pane size="50%" command="sh" { 42 | args "-c" "sleep 2 && task am-dbg -- --dir tmp -l localhost:6831 --fwd-data localhost:6832 --fwd-data localhost:6833 --output-diagrams 3 --tail --output-clients" 43 | } 44 | pane size="25%" command="sh" { 45 | args "-c" "task am-dbg -- -l localhost:6832 --view-narrow --view-timelines 1 --tail" 46 | } 47 | pane size="25%" command="sh" { 48 | args "-c" "task am-dbg -- -l localhost:6833 --view-narrow --view-timelines 1 --tail" 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/history/test/history_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | 9 | amhist "github.com/pancsta/asyncmachine-go/pkg/history" 10 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 11 | amss "github.com/pancsta/asyncmachine-go/pkg/states" 12 | ) 13 | 14 | func TestTrack(t *testing.T) { 15 | // configs 16 | rounds := 50 17 | onErr := func(err error) { 18 | t.Error(err) 19 | } 20 | cfg := amhist.Config{ 21 | TrackedStates: am.S{ss.Start}, 22 | } 23 | 24 | // init machine and history 25 | ctx := context.Background() 26 | mach := am.New(ctx, amss.BasicSchema, &am.Opts{Id: "MyMach1"}) 27 | mem, err := amhist.NewMemory(ctx, nil, mach, cfg, onErr) 28 | require.NoError(t, err) 29 | defer func() { 30 | require.NoError(t, mem.Dispose()) 31 | }() 32 | 33 | // common test 34 | AssertBasics(t, mem, rounds) 35 | } 36 | 37 | func TestTrackMany(t *testing.T) { 38 | // configs 39 | onErr := func(err error) { 40 | t.Error(err) 41 | } 42 | cfg := amhist.Config{ 43 | TrackedStates: am.S{ss.Start}, 44 | MaxRecords: 10e6, 45 | } 46 | 47 | // init machine and history 48 | ctx := context.Background() 49 | mach := am.New(ctx, amss.BasicSchema, &am.Opts{Id: "MyMach1"}) 50 | mem, err := amhist.NewMemory(ctx, nil, mach, cfg, onErr) 51 | require.NoError(t, err) 52 | defer func() { 53 | require.NoError(t, mem.Dispose()) 54 | }() 55 | 56 | // common test 57 | rounds := 50_000 58 | AssertBasics(t, mem, rounds) 59 | } 60 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /tools/relay/states/ss_relay.go: -------------------------------------------------------------------------------- 1 | // Package states contains a stateful schema-v2 for Relay. 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 | ssdbg "github.com/pancsta/asyncmachine-go/tools/debugger/states" 10 | ) 11 | 12 | // RelayStatesDef contains all the states of the Relay state machine. 13 | type RelayStatesDef struct { 14 | *am.StatesBase 15 | 16 | // inherit from BasicStatesDef 17 | *ss.BasicStatesDef 18 | // inherit from DisposedStatesDef 19 | *ss.DisposedStatesDef 20 | // inherit from [ssdbg.ServerStatesDef] 21 | *ssdbg.ServerStatesDef 22 | } 23 | 24 | // RelayGroupsDef contains all the state groups Relay state machine. 25 | type RelayGroupsDef struct { 26 | } 27 | 28 | // RelaySchema represents all relations and properties of RelayStates. 29 | var RelaySchema = SchemaMerge( 30 | // inherit from BasicStruct 31 | ss.BasicSchema, 32 | // inherit from DisposedStruct 33 | ss.DisposedSchema, 34 | // inherit from ServerSchema 35 | ssdbg.ServerSchema, 36 | am.Schema{ 37 | // TODO own states 38 | }) 39 | 40 | // EXPORTS AND GROUPS 41 | 42 | var ( 43 | ssV = am.NewStates(RelayStatesDef{}) 44 | sgV = am.NewStateGroups(RelayGroupsDef{}) 45 | 46 | // RelayStates contains all the states for the Relay machine. 47 | RelayStates = ssV 48 | // RelayGroups contains all the state groups for the Relay machine. 49 | RelayGroups = sgV 50 | ) 51 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /tools/visualizer/states/ss_visualizer.go: -------------------------------------------------------------------------------- 1 | // Package states contains a stateful schema-v2 for Visualizer. 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 | ssdbg "github.com/pancsta/asyncmachine-go/tools/debugger/states" 10 | ) 11 | 12 | // VisualizerStatesDef contains all the states of the Visualizer state machine. 13 | type VisualizerStatesDef struct { 14 | *am.StatesBase 15 | 16 | // inherit from BasicStatesDef 17 | *ss.BasicStatesDef 18 | // inherit from DisposedStatesDef 19 | *ss.DisposedStatesDef 20 | // inherit from [ssdbg.ServerStatesDef] 21 | *ssdbg.ServerStatesDef 22 | } 23 | 24 | // VisualizerGroupsDef contains all the state groups Visualizer state machine. 25 | type VisualizerGroupsDef struct { 26 | } 27 | 28 | // VisualizerSchema represents all relations and properties of VisualizerStates. 29 | var VisualizerSchema = SchemaMerge( 30 | // inherit from BasicStruct 31 | ss.BasicSchema, 32 | // inherit from DisposedStruct 33 | ss.DisposedSchema, 34 | // inherit from ServerSchema 35 | ssdbg.ServerSchema, 36 | am.Schema{ 37 | // TODO own states 38 | }) 39 | 40 | // EXPORTS AND GROUPS 41 | 42 | var ( 43 | ssV = am.NewStates(VisualizerStatesDef{}) 44 | sgV = am.NewStateGroups(VisualizerGroupsDef{}) 45 | 46 | // VisualizerStates contains all the states for the Visualizer machine. 47 | VisualizerStates = ssV 48 | // VisualizerGroups contains all the state groups for the Visualizer machine. 49 | VisualizerGroups = sgV 50 | ) 51 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /.github/workflows/go-basic.yml: -------------------------------------------------------------------------------- 1 | name: Go Test (basic) 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: actions/setup-go@v4 23 | with: 24 | go-version: ${{ matrix.go-version }} 25 | 26 | # Cache 27 | - name: Clear cache directory first before trying to restore from cache 28 | run: sudo rm -rf $(go env GOMODCACHE) && sudo rm -rf $(go env GOCACHE) 29 | shell: bash 30 | - name: Go Build Cache 31 | uses: actions/cache@v4 32 | with: 33 | path: ${{ steps.go-cache-paths.outputs.go-build }} 34 | key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} 35 | - name: Go Mod Cache 36 | uses: actions/cache@v4 37 | with: 38 | path: ${{ steps.go-cache-paths.outputs.go-mod }} 39 | key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} 40 | - name: Task Cache 41 | uses: actions/cache@v4 42 | with: 43 | path: ~/.local/bin/task 44 | key: ${{ runner.os }} 45 | 46 | # deps 47 | - name: Install Task 48 | run: ./scripts/dep-taskfile.sh 49 | - name: Install dependencies 50 | run: task install-deps-ci 51 | 52 | # run 53 | - name: Test 54 | run: task test 55 | -------------------------------------------------------------------------------- /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 the state machine handlers has timed 24 | // out. 25 | ErrHandlerTimeout string 26 | 27 | // Start indicates the machine should be working. Removing start can force 28 | // stop the machine. 29 | Start string 30 | // Ready indicates the machine meets criteria to perform work. 31 | Ready string 32 | // Healthcheck is a periodic request making sure that the machine is still 33 | // alive. 34 | Healthcheck string 35 | // Heartbeat is a periodic state that ensures the integrity of the machine. 36 | Heartbeat string 37 | } 38 | 39 | var BasicSchema = am.Schema{ 40 | // Errors 41 | 42 | ssB.Exception: {Multi: true}, 43 | ssB.ErrNetwork: { 44 | Multi: true, 45 | Require: S{Exception}, 46 | }, 47 | ssB.ErrHandlerTimeout: { 48 | Multi: true, 49 | Require: S{Exception}, 50 | }, 51 | 52 | // Basics 53 | 54 | ssB.Start: {}, 55 | ssB.Ready: {Require: S{ssB.Start}}, 56 | ssB.Healthcheck: {Multi: true}, 57 | ssB.Heartbeat: {}, 58 | } 59 | 60 | // EXPORTS AND GROUPS 61 | 62 | var ( 63 | ssB = am.NewStates(BasicStatesDef{}) 64 | 65 | // BasicStates contains all the states for the Basic machine. 66 | BasicStates = ssB 67 | ) 68 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.github/workflows/linter.yml: -------------------------------------------------------------------------------- 1 | name: Linter 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | # Disable this job 9 | # if: false 10 | 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | go-version: [ '1.23' ] 15 | 16 | steps: 17 | # Get values for cache paths to be used in later steps 18 | # Using the modern GITHUB_OUTPUT method 19 | - id: go-cache-paths 20 | run: | 21 | echo "go-build=$(go env GOCACHE)" >> $GITHUB_OUTPUT 22 | echo "go-mod=$(go env GOMODCACHE)" >> $GITHUB_OUTPUT 23 | 24 | # set up 25 | - uses: actions/checkout@v4 26 | # TODO no ruby 27 | - uses: ruby/setup-ruby@v1 28 | with: 29 | ruby-version: '3.3' 30 | - uses: actions/setup-go@v4 31 | with: 32 | go-version: ${{ matrix.go-version }} 33 | 34 | - name: GolangCI-Lint Cache 35 | uses: actions/cache@v4 36 | with: 37 | # Default cache path for golangci-lint on Linux 38 | path: ~/.cache/golangci-lint 39 | # Key on go.sum and the linter config file 40 | key: ${{ runner.os }}-golangci-lint-${{ hashFiles('**/go.sum', '.golangci.yml', '.golangci.yaml') }} 41 | restore-keys: | 42 | ${{ runner.os }}-golangci-lint- 43 | 44 | - name: Task Cache 45 | uses: actions/cache@v4 46 | with: 47 | path: ~/.local/bin/task 48 | key: ${{ runner.os }} 49 | 50 | # deps 51 | - name: Install Task 52 | run: ./scripts/dep-taskfile.sh 53 | - name: Install linter dependencies 54 | run: task install-deps-linter 55 | 56 | # run 57 | - name: Run linters 58 | run: task lint-ci -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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: actions/setup-go@v4 23 | with: 24 | go-version: ${{ matrix.go-version }} 25 | 26 | # Cache 27 | - name: Clear cache directory first before trying to restore from cache 28 | run: sudo rm -rf $(go env GOMODCACHE) && sudo rm -rf $(go env GOCACHE) 29 | shell: bash 30 | - name: Go Build Cache 31 | uses: actions/cache@v4 32 | with: 33 | path: ${{ steps.go-cache-paths.outputs.go-build }} 34 | key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} 35 | - name: Go Mod Cache 36 | uses: actions/cache@v4 37 | with: 38 | path: ${{ steps.go-cache-paths.outputs.go-mod }} 39 | key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} 40 | - name: Task Cache 41 | uses: actions/cache@v4 42 | with: 43 | path: ~/.local/bin/task 44 | key: ${{ runner.os }} 45 | 46 | # deps 47 | - name: Install Task 48 | run: ./scripts/dep-taskfile.sh 49 | - name: Install dependencies 50 | run: task install-deps-ci 51 | 52 | # run 53 | 54 | - name: Test /tools 55 | run: task test-path -- ./tools/... 56 | -------------------------------------------------------------------------------- /.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: actions/setup-go@v4 23 | with: 24 | go-version: ${{ matrix.go-version }} 25 | 26 | # Cache 27 | - name: Clear cache directory first before trying to restore from cache 28 | run: sudo rm -rf $(go env GOMODCACHE) && sudo rm -rf $(go env GOCACHE) 29 | shell: bash 30 | - name: Go Build Cache 31 | uses: actions/cache@v4 32 | with: 33 | path: ${{ steps.go-cache-paths.outputs.go-build }} 34 | key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} 35 | - name: Go Mod Cache 36 | uses: actions/cache@v4 37 | with: 38 | path: ${{ steps.go-cache-paths.outputs.go-mod }} 39 | key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} 40 | - name: Task Cache 41 | uses: actions/cache@v4 42 | with: 43 | path: ~/.local/bin/task 44 | key: ${{ runner.os }} 45 | 46 | # deps 47 | - name: Install Task 48 | run: ./scripts/dep-taskfile.sh 49 | - name: Install dependencies 50 | run: task install-deps-ci 51 | 52 | # run 53 | 54 | - name: Test /pkg/node 55 | run: task test-path -- ./pkg/node/... 56 | -------------------------------------------------------------------------------- /.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: actions/setup-go@v4 23 | with: 24 | go-version: ${{ matrix.go-version }} 25 | 26 | # Cache 27 | - name: Clear cache directory first before trying to restore from cache 28 | run: sudo rm -rf $(go env GOMODCACHE) && sudo rm -rf $(go env GOCACHE) 29 | shell: bash 30 | - name: Go Build Cache 31 | uses: actions/cache@v4 32 | with: 33 | path: ${{ steps.go-cache-paths.outputs.go-build }} 34 | key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} 35 | - name: Go Mod Cache 36 | uses: actions/cache@v4 37 | with: 38 | path: ${{ steps.go-cache-paths.outputs.go-mod }} 39 | key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} 40 | - name: Task Cache 41 | uses: actions/cache@v4 42 | with: 43 | path: ~/.local/bin/task 44 | key: ${{ runner.os }} 45 | 46 | # deps 47 | - name: Install Task 48 | run: ./scripts/dep-taskfile.sh 49 | - name: Install dependencies 50 | run: task install-deps-ci 51 | 52 | # run 53 | 54 | - name: Test /pkg/rpc 55 | run: task test-path -- ./pkg/rpc/... 56 | -------------------------------------------------------------------------------- /.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: actions/setup-go@v4 23 | with: 24 | go-version: ${{ matrix.go-version }} 25 | 26 | # Cache 27 | - name: Clear cache directory first before trying to restore from cache 28 | run: sudo rm -rf $(go env GOMODCACHE) && sudo rm -rf $(go env GOCACHE) 29 | shell: bash 30 | - name: Go Build Cache 31 | uses: actions/cache@v4 32 | with: 33 | path: ${{ steps.go-cache-paths.outputs.go-build }} 34 | key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} 35 | - name: Go Mod Cache 36 | uses: actions/cache@v4 37 | with: 38 | path: ${{ steps.go-cache-paths.outputs.go-mod }} 39 | key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} 40 | - name: Task Cache 41 | uses: actions/cache@v4 42 | with: 43 | path: ~/.local/bin/task 44 | key: ${{ runner.os }} 45 | 46 | # deps 47 | - name: Install Task 48 | run: ./scripts/dep-taskfile.sh 49 | - name: Install dependencies 50 | run: task install-deps-ci 51 | 52 | # run 53 | 54 | - name: Test /pkg/pubsub 55 | run: task test-path -- ./pkg/pubsub/... 56 | -------------------------------------------------------------------------------- /.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: actions/setup-go@v4 23 | with: 24 | go-version: ${{ matrix.go-version }} 25 | 26 | # Cache 27 | - name: Clear cache directory first before trying to restore from cache 28 | run: sudo rm -rf $(go env GOMODCACHE) && sudo rm -rf $(go env GOCACHE) 29 | shell: bash 30 | - name: Go Build Cache 31 | uses: actions/cache@v4 32 | with: 33 | path: ${{ steps.go-cache-paths.outputs.go-build }} 34 | key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} 35 | - name: Go Mod Cache 36 | uses: actions/cache@v4 37 | with: 38 | path: ${{ steps.go-cache-paths.outputs.go-mod }} 39 | key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} 40 | - name: Task Cache 41 | uses: actions/cache@v4 42 | with: 43 | path: ~/.local/bin/task 44 | key: ${{ runner.os }} 45 | 46 | # deps 47 | - name: Install Task 48 | run: ./scripts/dep-taskfile.sh 49 | - name: Install dependencies 50 | run: task install-deps-ci 51 | 52 | # run 53 | 54 | - name: Test /pkg/states 55 | run: task test-path -- ./pkg/states/... 56 | -------------------------------------------------------------------------------- /.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: actions/setup-go@v4 23 | with: 24 | go-version: ${{ matrix.go-version }} 25 | 26 | # Cache 27 | - name: Clear cache directory first before trying to restore from cache 28 | run: sudo rm -rf $(go env GOMODCACHE) && sudo rm -rf $(go env GOCACHE) 29 | shell: bash 30 | - name: Go Build Cache 31 | uses: actions/cache@v4 32 | with: 33 | path: ${{ steps.go-cache-paths.outputs.go-build }} 34 | key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} 35 | - name: Go Mod Cache 36 | uses: actions/cache@v4 37 | with: 38 | path: ${{ steps.go-cache-paths.outputs.go-mod }} 39 | key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} 40 | - name: Task Cache 41 | uses: actions/cache@v4 42 | with: 43 | path: ~/.local/bin/task 44 | key: ${{ runner.os }} 45 | 46 | # deps 47 | - name: Install Task 48 | run: ./scripts/dep-taskfile.sh 49 | - name: Install dependencies 50 | run: task install-deps-ci 51 | 52 | # run 53 | 54 | - name: Test /pkg/helpers 55 | run: task test-path -- ./pkg/helpers/... 56 | -------------------------------------------------------------------------------- /.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: actions/setup-go@v4 23 | with: 24 | go-version: ${{ matrix.go-version }} 25 | 26 | # Cache 27 | - name: Clear cache directory first before trying to restore from cache 28 | run: sudo rm -rf $(go env GOMODCACHE) && sudo rm -rf $(go env GOCACHE) 29 | shell: bash 30 | - name: Go Build Cache 31 | uses: actions/cache@v4 32 | with: 33 | path: ${{ steps.go-cache-paths.outputs.go-build }} 34 | key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} 35 | - name: Go Mod Cache 36 | uses: actions/cache@v4 37 | with: 38 | path: ${{ steps.go-cache-paths.outputs.go-mod }} 39 | key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} 40 | - name: Task Cache 41 | uses: actions/cache@v4 42 | with: 43 | path: ~/.local/bin/task 44 | key: ${{ runner.os }} 45 | 46 | # deps 47 | - name: Install Task 48 | run: ./scripts/dep-taskfile.sh 49 | - name: Install dependencies 50 | run: task install-deps-ci 51 | 52 | # run 53 | 54 | - name: Test /pkg/history 55 | run: task test-path -- ./pkg/history/... 56 | -------------------------------------------------------------------------------- /.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: actions/setup-go@v4 23 | with: 24 | go-version: ${{ matrix.go-version }} 25 | 26 | # Cache 27 | - name: Clear cache directory first before trying to restore from cache 28 | run: sudo rm -rf $(go env GOMODCACHE) && sudo rm -rf $(go env GOCACHE) 29 | shell: bash 30 | - name: Go Build Cache 31 | uses: actions/cache@v4 32 | with: 33 | path: ${{ steps.go-cache-paths.outputs.go-build }} 34 | key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} 35 | - name: Go Mod Cache 36 | uses: actions/cache@v4 37 | with: 38 | path: ${{ steps.go-cache-paths.outputs.go-mod }} 39 | key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} 40 | - name: Task Cache 41 | uses: actions/cache@v4 42 | with: 43 | path: ~/.local/bin/task 44 | key: ${{ runner.os }} 45 | 46 | # deps 47 | - name: Install Task 48 | run: ./scripts/dep-taskfile.sh 49 | - name: Install dependencies 50 | run: task install-deps-ci 51 | 52 | # run 53 | 54 | - name: Test /pkg/machine 55 | run: task test-path -- ./pkg/machine/... 56 | -------------------------------------------------------------------------------- /.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: actions/setup-go@v4 23 | with: 24 | go-version: ${{ matrix.go-version }} 25 | 26 | # Cache 27 | - name: Clear cache directory first before trying to restore from cache 28 | run: sudo rm -rf $(go env GOMODCACHE) && sudo rm -rf $(go env GOCACHE) 29 | shell: bash 30 | - name: Go Build Cache 31 | uses: actions/cache@v4 32 | with: 33 | path: ${{ steps.go-cache-paths.outputs.go-build }} 34 | key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} 35 | - name: Go Mod Cache 36 | uses: actions/cache@v4 37 | with: 38 | path: ${{ steps.go-cache-paths.outputs.go-mod }} 39 | key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} 40 | - name: Task Cache 41 | uses: actions/cache@v4 42 | with: 43 | path: ~/.local/bin/task 44 | key: ${{ runner.os }} 45 | 46 | # deps 47 | - name: Install Task 48 | run: ./scripts/dep-taskfile.sh 49 | - name: Install dependencies 50 | run: task install-deps-ci 51 | 52 | # run 53 | 54 | - name: Test /pkg/telemetry 55 | run: task test-path -- ./pkg/telemetry/... 56 | -------------------------------------------------------------------------------- /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.SetEnvLogLevel(am.LogOps) 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 | -------------------------------------------------------------------------------- /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 Generator 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 of the Generator state 29 | // machine. 30 | type GeneratorGroupsDef struct { 31 | Inherit S 32 | } 33 | 34 | // GeneratorSchema represents all relations and properties of GeneratorStates. 35 | var GeneratorSchema = am.Schema{ 36 | ssG.InheritBasic: {}, 37 | ssG.InheritConnected: {Add: S{ssG.GroupsInherited}}, 38 | ssG.InheritDisposed: {}, 39 | 40 | ssG.InheritRpcWorker: {}, 41 | ssG.InheritNodeWorker: { 42 | Add: S{ssG.GroupsInherited}, 43 | Remove: S{ssG.InheritRpcWorker}, 44 | }, 45 | 46 | ssG.Inherit: {Auto: true}, 47 | ssG.GroupsLocal: {}, 48 | ssG.GroupsInherited: {}, 49 | ssG.Groups: {Auto: true}, 50 | } 51 | 52 | // EXPORTS AND GROUPS 53 | 54 | var ( 55 | ssG = am.NewStates(GeneratorStatesDef{}) 56 | sgG = am.NewStateGroups(GeneratorGroupsDef{ 57 | Inherit: S{ssG.InheritBasic, ssG.InheritConnected, ssG.InheritRpcWorker, 58 | ssG.InheritNodeWorker, ssG.InheritDisposed}, 59 | }) 60 | 61 | // GeneratorStates contains all the states for the Generator machine. 62 | GeneratorStates = ssG 63 | // GeneratorGroups contains all the state groups for the Generator machine. 64 | GeneratorGroups = sgG 65 | ) 66 | -------------------------------------------------------------------------------- /config/dashboards/dash-full.kdl: -------------------------------------------------------------------------------- 1 | keybinds clear-defaults=true { 2 | locked { 3 | // navi 4 | bind "Alt Left" { 5 | MoveFocusOrTab "Left"; } 6 | bind "Alt Right" { 7 | MoveFocusOrTab "Right"; } 8 | bind "Alt Down" { 9 | MoveFocus "Down"; } 10 | bind "Alt Up" { 11 | MoveFocus "Up"; } 12 | bind "Alt =" { 13 | Resize "Increase"; } 14 | 15 | // resize 16 | bind "Alt =" "Alt +" { 17 | Resize "Increase"; } 18 | bind "Alt -" { 19 | Resize "Decrease"; } 20 | 21 | 22 | bind "Ctrl q" { 23 | Quit ; } 24 | } 25 | } 26 | 27 | pane_frames false 28 | copy_on_select false 29 | pane_frames false 30 | ui { 31 | pane_frames { 32 | hide_session_name true 33 | rounded_corners true 34 | } 35 | } 36 | show_release_notes false 37 | show_startup_tips false 38 | 39 | layout { 40 | pane split_direction="vertical" { 41 | 42 | pane size="50%" command="sh" { 43 | args "-c" "sleep 2 && task am-dbg -- --dir tmp -l localhost:6831 --fwd-data localhost:6832,localhost:6833,localhost:6834 --output-diagrams 3 --tail --output-clients" 44 | } 45 | 46 | pane size="50%" split_direction="horizontal" { 47 | pane size="50%" split_direction="vertical" { 48 | pane size="50%" command="sh" { 49 | args "-c" "task am-dbg -- -l localhost:6832 --view-narrow --view-timelines 0 --view matrix --tail" 50 | } 51 | pane size="50%" command="sh" { 52 | args "-c" "task am-dbg -- -l localhost:6833 --view-narrow --view-timelines 0 --view-rain --view matrix --tail" 53 | } 54 | } 55 | pane size="50%" command="sh" { 56 | args "-c" "task am-dbg -- -l localhost:6834 --view-narrow --view-timelines 0 --tail --view-timelines 1 --view-reader" 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /config/dashboards/dash-wide-narrow-repl.kdl: -------------------------------------------------------------------------------- 1 | keybinds clear-defaults=true { 2 | locked { 3 | // navi 4 | bind "Alt Left" { 5 | MoveFocusOrTab "Left"; } 6 | bind "Alt Right" { 7 | MoveFocusOrTab "Right"; } 8 | bind "Alt Down" { 9 | MoveFocus "Down"; } 10 | bind "Alt Up" { 11 | MoveFocus "Up"; } 12 | bind "Alt =" { 13 | Resize "Increase"; } 14 | 15 | // resize 16 | bind "Alt =" "Alt +" { 17 | Resize "Increase"; } 18 | bind "Alt -" { 19 | Resize "Decrease"; } 20 | 21 | 22 | bind "Ctrl q" { 23 | Quit ; } 24 | } 25 | } 26 | 27 | pane_frames false 28 | copy_on_select false 29 | pane_frames false 30 | ui { 31 | pane_frames { 32 | hide_session_name true 33 | rounded_corners true 34 | } 35 | } 36 | show_release_notes false 37 | show_startup_tips false 38 | 39 | layout { 40 | pane split_direction="vertical" { 41 | pane size="75%" split_direction="horizontal" { 42 | pane size="85%" command="sh" { 43 | // args "-c" "sleep 2 && task am-dbg -- --dir tmp -l localhost:6831 --fwd-data localhost:6832 --output-diagrams 3 --tail --output-clients" 44 | // args "-c" "sleep 2 && task am-dbg -- --dir tmp -l localhost:6831 --fwd-data localhost:6832 --output-diagrams 3 --tail --output-clients --import-data am-dbg-dump.gob.br" 45 | args "-c" "sleep 2 && task am-dbg -- --dir tmp -l localhost:6831 --fwd-data localhost:6832 --output-diagrams 3 --tail --output-clients --am-dbg-addr localhost:9913" 46 | } 47 | 48 | pane size="15%" command="sh" { 49 | args "-c" "task arpc -- -d $AM_REPL_DIR --watch" 50 | // args "-c" "task arpc -- -d $AM_REPL_DIR --watch --am-dbg-addr localhost:9913 --log-level 2" 51 | } 52 | } 53 | 54 | pane size="25%" command="sh" { 55 | args "-c" "task am-dbg -- -l localhost:6832 --view-narrow --view-timelines 0 --tail --view-timelines 1" 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /tools/cmd/arpc/cmd_arpc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "os/signal" 7 | "time" 8 | 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 | // amhelp.EnableDebugging(false) 21 | // amhelp.EnableDebugging(true) 22 | } 23 | 24 | type S = am.S 25 | type T = am.Time 26 | 27 | func main() { 28 | ctx := context.Background() 29 | 30 | var cliArgs []string 31 | var connArgs []string 32 | 33 | osArgs := os.Args[1:] 34 | for i, v := range osArgs { 35 | if v == "--" && len(os.Args) > i+1 { 36 | cliArgs = os.Args[i+2:] 37 | connArgs = os.Args[1 : i+1] 38 | break 39 | } 40 | } 41 | 42 | // repl 43 | id := "repl" 44 | if randomId { 45 | id = "repl-" + utils.RandId(4) 46 | } 47 | r, err := repl.New(ctx, id) 48 | if err != nil { 49 | panic(err) 50 | } 51 | rootCmd := repl.NewRootCommand(r, cliArgs, osArgs) 52 | if len(connArgs) > 0 { 53 | rootCmd.SetArgs(connArgs) 54 | } 55 | r.Cmd = rootCmd 56 | 57 | // cobra 58 | err = rootCmd.Execute() 59 | if err != nil { 60 | if amhelp.IsTelemetry() { 61 | time.Sleep(time.Second) 62 | os.Exit(1) 63 | } 64 | } 65 | 66 | sigCh := make(chan os.Signal, 1) 67 | signal.Notify(sigCh, os.Interrupt) 68 | 69 | // waits 70 | select { 71 | 72 | // signal 73 | case <-sigCh: 74 | r.Mach.Add1(ss.Disposing, nil) 75 | // CLI 76 | case <-r.Mach.WhenNot1(ss.ReplMode, nil): 77 | // exit 78 | case <-r.Mach.When1(ss.Disposed, nil): 79 | } 80 | 81 | // fmt.Println("bye") 82 | r.Mach.Dispose() 83 | 84 | if amhelp.IsTelemetry() { 85 | time.Sleep(time.Second) 86 | } 87 | 88 | os.Exit(0) 89 | } 90 | -------------------------------------------------------------------------------- /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/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 | ssam "github.com/pancsta/asyncmachine-go/pkg/states" 7 | ) 8 | 9 | // WorkerStatesDef contains all the states of the Worker state machine. 10 | type WorkerStatesDef struct { 11 | *am.StatesBase 12 | 13 | Event string 14 | Value1 string 15 | Value2 string 16 | Value3 string 17 | CallOp string 18 | 19 | // inherit from BasicSchema 20 | *ssam.BasicStatesDef 21 | // inherit from WorkerStatesDef 22 | *ssrpc.WorkerStatesDef 23 | } 24 | 25 | // WorkerGroupsDef contains all the state groups of the Worker state machine. 26 | type WorkerGroupsDef struct { 27 | 28 | // Values group contains mutually exclusive values. 29 | Values S 30 | } 31 | 32 | // WorkerSchema represents all relations and properties of WorkerStates. 33 | var WorkerSchema = SchemaMerge( 34 | // inherit from BasicSchema 35 | ssam.BasicSchema, 36 | // inherit from WorkerSchema 37 | ssrpc.WorkerSchema, 38 | am.Schema{ 39 | 40 | // ops 41 | ws.CallOp: { 42 | Multi: true, 43 | Require: S{ws.Start}, 44 | }, 45 | 46 | // events 47 | ws.Event: { 48 | Multi: true, 49 | Require: S{ws.Start}, 50 | }, 51 | 52 | // values 53 | ws.Value1: {Remove: wg.Values}, 54 | ws.Value2: {Remove: wg.Values}, 55 | ws.Value3: {Remove: wg.Values}, 56 | }) 57 | 58 | // EXPORTS AND GROUPS 59 | 60 | var ( 61 | // ws is worker states from WorkerStatesDef. 62 | ws = am.NewStates(WorkerStatesDef{}) 63 | 64 | // wg is worker groups from WorkerGroupsDef. 65 | wg = am.NewStateGroups(WorkerGroupsDef{ 66 | Values: S{ws.Value1, ws.Value2, ws.Value3}, 67 | }) 68 | 69 | // WorkerStates contains all the states for the Worker machine. 70 | WorkerStates = ws 71 | 72 | // WorkerGroups contains all the state groups for the Worker machine. 73 | WorkerGroups = wg 74 | ) 75 | -------------------------------------------------------------------------------- /pkg/x/history/frostdb/frostdb_test.go: -------------------------------------------------------------------------------- 1 | package frostdb 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/require" 9 | 10 | amhelp "github.com/pancsta/asyncmachine-go/pkg/helpers" 11 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 12 | ) 13 | 14 | func TestFrostdbTrack(t *testing.T) { 15 | if amhelp.IsTestRunner() { 16 | t.Skip("no experimental in test runner") 17 | } 18 | 19 | // Create memory storage 20 | ctx := context.Background() 21 | mem, err := NewMemory(ctx) 22 | require.NoError(t, err) 23 | 24 | // Create a machine with ss states 25 | mach := am.New(ctx, BasicSchema, &am.Opts{Id: "MyMach1"}) 26 | onErr := func(err error) { 27 | t.Error(err) 28 | } 29 | 30 | // TODO NewCommon, DebugMachEnv 31 | tr, err := mem.Track(onErr, mach, nil, nil, 10^6) 32 | require.NoError(t, err) 33 | 34 | rounds := 500 35 | start := time.Now() 36 | 37 | for i := range rounds { 38 | if i%100 == 0 { 39 | t.Logf("i: %d", i) 40 | } 41 | mach.Toggle1(ssB.Start, nil) 42 | } 43 | t.Logf("elapsed1: %s", time.Since(start)) 44 | 45 | // wait for the Tracer to finish 46 | require.Eventually(t, func() bool { 47 | return tr.Active.Load() == 0 48 | }, time.Second*10, time.Millisecond*100, "too slow") 49 | 50 | t.Logf("elapsed2: %s", time.Since(start)) 51 | 52 | err = tr.db.Snapshot(ctx) 53 | require.NoError(t, err) 54 | err = tr.db.Close() 55 | require.NoError(t, err) 56 | 57 | // // Create a new query engine to retrieve data and print the results 58 | // engine := query.NewEngine(memory.DefaultAllocator, 59 | // database.TableProvider()) 60 | // _ = engine.ScanTable("simple_table"). 61 | // Project(logicalplan.DynCol("names")). 62 | // Filter( 63 | // logicalplan.Col("names.first_name").Eq( 64 | // logicalplan.Literal("Frederic")), 65 | // ).Execute(context.Background(), 66 | // func(_ context.Context, r arrow.RecordBatch) error { 67 | // fmt.Println(r) 68 | // return nil 69 | // }) 70 | } 71 | -------------------------------------------------------------------------------- /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/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 | amxhelp "github.com/pancsta/asyncmachine-go/pkg/x/helpers" 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.SetEnvLogLevel(am.LogChanges) 22 | } 23 | 24 | func main() { 25 | ctx := context.Background() 26 | 27 | states := am.Schema{ 28 | // task start state 29 | "Task": {Require: am.S{"Start"}}, 30 | // task done state 31 | "TaskDone": {}, 32 | 33 | // rest 34 | 35 | "Start": {}, 36 | "Ready": { 37 | Auto: true, 38 | Require: am.S{"TaskDone"}, 39 | }, 40 | "Healthcheck": {Multi: true}, 41 | } 42 | names := am.S{"Start", "Task", "TaskDone", "Ready", "Healthcheck", am.StateException} 43 | 44 | // init state machine 45 | mach, err := am.NewCommon(ctx, "fan", states, names, &am.ExceptionHandler{}, nil, &am.Opts{ 46 | LogLevel: am.LogOps, 47 | }) 48 | if err != nil { 49 | panic(err) 50 | } 51 | amhelp.MachDebugEnv(mach) 52 | 53 | // define a task func 54 | fn := func(num int, state, stateDone string) { 55 | ctx := mach.NewStateCtx(state) 56 | go func() { 57 | if ctx.Err() != nil { 58 | return // expired 59 | } 60 | amhelp.Wait(ctx, time.Second) 61 | if ctx.Err() != nil { 62 | return // expired 63 | } 64 | mach.Add1(stateDone, nil) 65 | }() 66 | } 67 | 68 | // create task states 69 | // 10 tasks, 3 running concurrently 70 | _, err = amxhelp.FanOutIn(mach, "Task", 15, 3, fn) 71 | if err != nil { 72 | panic(err) 73 | } 74 | 75 | // start and wait 76 | mach.Add(am.S{"Start", "Task"}, nil) 77 | <-mach.When1("Ready", nil) 78 | 79 | // end 80 | println("done") 81 | 82 | // debug 83 | time.Sleep(time.Second) 84 | } 85 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /scripts/gen_website/sitemap/sitemap.go: -------------------------------------------------------------------------------- 1 | package sitemap 2 | 3 | type Entry struct { 4 | Url string 5 | Path string 6 | SkipMenu bool 7 | } 8 | 9 | var MainMenu = []Entry{ 10 | {Url: "", Path: "README.md"}, 11 | {Url: "examples", Path: "examples/README.md"}, 12 | {Url: "manual", Path: "docs/manual.md"}, 13 | {"", "", false}, 14 | {Url: "machine", Path: "pkg/machine/README.md"}, 15 | {Url: "states", Path: "pkg/states/README.md"}, 16 | {Url: "helpers", Path: "pkg/helpers/README.md"}, 17 | {Url: "telemetry", Path: "pkg/telemetry/README.md"}, 18 | {Url: "history", Path: "pkg/history/README.md"}, 19 | {"", "", false}, 20 | {Url: "rpc", Path: "pkg/rpc/README.md"}, 21 | {Url: "integrations", Path: "pkg/integrations/README.md"}, 22 | {Url: "node", Path: "pkg/node/README.md"}, 23 | {Url: "pubsub", Path: "pkg/pubsub/README.md"}, 24 | {"", "", false}, 25 | {Url: "am-gen", Path: "tools/cmd/am-gen/README.md"}, 26 | {Url: "am-dbg", Path: "tools/cmd/am-dbg/README.md"}, 27 | {Url: "arpc", Path: "tools/cmd/arpc/README.md"}, 28 | {Url: "am-vis", Path: "tools/cmd/am-vis/README.md"}, 29 | {Url: "am-relay", Path: "tools/cmd/am-relay/README.md"}, 30 | 31 | // rest (non-menu) 32 | {Url: "faq", Path: "FAQ.md", SkipMenu: true}, 33 | {Url: "changelog", Path: "CHANGELOG.md", SkipMenu: true}, 34 | {Url: "env-configs", Path: "docs/env-configs.md", SkipMenu: true}, 35 | {Url: "tree-state-source", 36 | Path: "examples/tree_state_source/README.md", SkipMenu: true}, 37 | {Url: "benchmark-grpc", 38 | Path: "examples/benchmark_grpc/README.md", SkipMenu: true}, 39 | {Url: "benchmark-libp2p-pubsub", 40 | Path: "examples/benchmark_libp2p_pubsub/README.md", SkipMenu: true}, 41 | {Url: "tools-debugger", 42 | Path: "tools/debugger/README.md", SkipMenu: true}, 43 | {Url: "benchmark-state-source", 44 | Path: "examples/benchmark_state_source/README.md", SkipMenu: true}, 45 | {Url: "arpc-howto", Path: "pkg/rpc/HOWTO.md", SkipMenu: true}, 46 | {Url: "cookbook", Path: "docs/cookbook.md", SkipMenu: true}, 47 | {Url: "diagrams", Path: "docs/diagrams.md", SkipMenu: true}, 48 | {Url: "roadmap", Path: "ROADMAP.md", SkipMenu: true}, 49 | {Url: "breaking-changes", Path: "BREAKING.md", SkipMenu: true}, 50 | } 51 | -------------------------------------------------------------------------------- /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 | } 84 | -------------------------------------------------------------------------------- /pkg/x/helpers/x_help_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 | statesStruct["Bump1"] = am.State{} 22 | err := m1.SetSchema(statesStruct, 23 | S{"A", "B", "C", "D", "Bump1", am.StateException}) 24 | assert.NoError(t, err) 25 | 26 | // mutate & assert 27 | m1.Add(S{"A", "B"}, nil) 28 | m1.Add(S{"A", "B"}, nil) 29 | assertTime(t, m1, S{"A", "B", "C", "D"}, T{1, 3, 0, 0}, 30 | "m1 clocks mismatch") 31 | 32 | // m2 init 33 | m2 := NewNoRels(t, nil) 34 | statesStruct = m1.Schema() 35 | statesStruct["B"] = am.State{Multi: true} 36 | statesStruct["Bump1"] = am.State{} 37 | err = m2.SetSchema(statesStruct, S{"A", "B", "C", "D", "Bump1", 38 | am.StateException}) 39 | assert.NoError(t, err) 40 | 41 | // mutate & assert 42 | m2.Add(S{"A", "B"}, nil) 43 | m2.Add(S{"A", "B"}, nil) 44 | m2.Add(S{"A", "B", "C"}, nil) 45 | m2.Set(S{"D"}, nil) 46 | assertTime(t, m2, S{"A", "B", "C", "D"}, T{2, 6, 2, 1}, 47 | "m2 clocks mismatch") 48 | 49 | matrix, err := TimeMatrix([]*am.Machine{m1, m2}) 50 | assert.NoError(t, err) 51 | assert.Equal(t, T{1, 3, 0, 0, 0, 0}, matrix[0]) 52 | assert.Equal(t, T{2, 6, 2, 1, 0, 0}, matrix[1]) 53 | } 54 | 55 | // /// helpers 56 | // TODO extract test helpers to internal/testing 57 | 58 | // NewNoRels creates a new machine with no relations between states. 59 | func NewNoRels(t *testing.T, initialState am.S) *am.Machine { 60 | m := am.New(context.Background(), am.Schema{ 61 | "A": {}, 62 | "B": {}, 63 | "C": {}, 64 | "D": {}, 65 | }, nil) 66 | m.SemLogger().SetLogger(func(i am.LogLevel, msg string, args ...any) { 67 | t.Logf(msg, args...) 68 | }) 69 | if amhelp.IsDebug() { 70 | m.SemLogger().SetLevel(am.LogEverything) 71 | m.HandlerTimeout = 2 * time.Minute 72 | } 73 | if initialState != nil { 74 | m.Set(initialState, nil) 75 | } 76 | return m 77 | } 78 | 79 | func assertTime(t *testing.T, m *am.Machine, states S, time T, 80 | msgAndArgs ...interface{}, 81 | ) { 82 | assert.Equal(t, m.Time(states), time, msgAndArgs...) 83 | } 84 | -------------------------------------------------------------------------------- /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.RelsNodeWorkerSchema, &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.SemLogger().SetLevel(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 | -------------------------------------------------------------------------------- /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.SetEnvLogLevel(am.LogOps) 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.SemLogger().SetArgsMapper(am.NewArgsMapper([]string{"log"}, 0)) 73 | worker.SemLogger().SetLevel(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 | -------------------------------------------------------------------------------- /tools/debugger/utils_test.go: -------------------------------------------------------------------------------- 1 | package debugger 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/pancsta/asyncmachine-go/tools/debugger/server" 10 | ) 11 | 12 | func TestHumanSort(t *testing.T) { 13 | data := []string{ 14 | "_nco-dev-test-210946-00e4ff1", 15 | "_rs-nco-dev-test-210946-00e4ff1", 16 | "_rc-con-dev-test-2870ef", 17 | "_rs-nw-loc-dev-test-6791e5", 18 | "_rc-con-dev-test-6791e5", 19 | "_ns-dev-test-210946-0", 20 | "_rs-ns-pub-dev-test-210946-0", 21 | "_rs-ns-loc-dev-test-210946-0", 22 | "_rs-nco-dev-test-210946-0e7608f", 23 | "_rc-ns-dev-test-p-36317", 24 | "_rs-nw-loc-dev-test-b55a1b", 25 | "_nco-dev-test-210946-0c2bbae", 26 | "_rc-ns-dev-test-p-44331", 27 | "_rs-nw-loc-dev-test-2870ef", 28 | "_nw-dev-test-2870ef", 29 | "_nw-dev-test-6791e5", 30 | "_nco-dev-test-210946-0e7608f", 31 | "_nw-dev-test-b55a1b", 32 | "_rc-con-dev-test-b55a1b", 33 | "_rs-nco-dev-test-210946-0c2bbae", 34 | } 35 | 36 | expected := []string{ 37 | "_nco-dev-test-210946-00e4ff1", 38 | "_nco-dev-test-210946-0c2bbae", 39 | "_nco-dev-test-210946-0e7608f", 40 | 41 | "_ns-dev-test-210946-0", 42 | 43 | "_nw-dev-test-2870ef", 44 | "_nw-dev-test-6791e5", 45 | "_nw-dev-test-b55a1b", 46 | 47 | "_rc-con-dev-test-2870ef", 48 | "_rc-con-dev-test-6791e5", 49 | "_rc-con-dev-test-b55a1b", 50 | "_rc-ns-dev-test-p-36317", 51 | "_rc-ns-dev-test-p-44331", 52 | "_rs-nco-dev-test-210946-00e4ff1", 53 | "_rs-nco-dev-test-210946-0c2bbae", 54 | "_rs-nco-dev-test-210946-0e7608f", 55 | "_rs-ns-loc-dev-test-210946-0", 56 | "_rs-ns-pub-dev-test-210946-0", 57 | "_rs-nw-loc-dev-test-2870ef", 58 | "_rs-nw-loc-dev-test-6791e5", 59 | "_rs-nw-loc-dev-test-b55a1b", 60 | } 61 | 62 | humanSort(data) 63 | join := strings.Join(data, "\n") 64 | 65 | // debug 66 | // t.Log("\n" + join) 67 | 68 | assert.Equal(t, strings.Join(expected, "\n"), join) 69 | } 70 | 71 | func TestHadErrSince(t *testing.T) { 72 | c := &Client{ 73 | Client: &server.Client{ 74 | Errors: []int{100, 50, 5, 1}, 75 | }, 76 | } 77 | 78 | assert.False(t, c.HadErrSinceTx(300, 5), "tx: %d, dist: %d", 300, 5) 79 | assert.False(t, c.HadErrSinceTx(105, 3), "tx: %d, dist: %d", 105, 3) 80 | 81 | assert.True(t, c.HadErrSinceTx(55, 10), "tx: %d, dist: %d", 55, 10) 82 | assert.True(t, c.HadErrSinceTx(6, 2), "tx: %d, dist: %d", 6, 2) 83 | assert.True(t, c.HadErrSinceTx(100, 2), "tx: %d, dist: %d", 100, 2) 84 | assert.True(t, c.HadErrSinceTx(1, 1), "tx: %d, dist: %d", 1, 1) 85 | } 86 | -------------------------------------------------------------------------------- /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 | amhelp "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(amhelp.EnvAmLogFile, "1") 39 | dbg, err := amtest.NewDbgWorker(true, debugger.Opts{ 40 | AddrRpc: *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 | amhelp.MachDebug(s.Mach, amDbgAddr, logLvl, false, 52 | amhelp.SemConfig(true)) 53 | 54 | // tear down 55 | defer func() { 56 | if amDbgAddr != "" { 57 | // cool off am-dbg and free the ports 58 | time.Sleep(100 * time.Millisecond) 59 | } 60 | }() 61 | 62 | // am-dbg server (used for testing live connections) 63 | if *serverAddr != "" { 64 | go server.StartRpc(dbg.Mach, *serverAddr, nil, nil, false) 65 | } 66 | 67 | // start with a timeout 68 | readyCtx, cancel := context.WithTimeout(ctx, 3*time.Second) 69 | defer cancel() 70 | 71 | // server start 72 | s.Start() 73 | select { 74 | case <-s.Mach.WhenErr(readyCtx): 75 | err := s.Mach.Err() 76 | if readyCtx.Err() != nil { 77 | err = readyCtx.Err() 78 | } 79 | panic(err) 80 | case <-s.Mach.When1(ssrpc.ServerStates.RpcReady, readyCtx): 81 | } 82 | 83 | // wait till the end 84 | select { 85 | case <-dbg.Mach.WhenDisposed(): 86 | // user exit 87 | case <-dbg.Mach.WhenNot1(ssdbg.Start, nil): 88 | dbg.Dispose() 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /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.StateException}, 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 | -------------------------------------------------------------------------------- /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.SetEnvLogLevel(am.LogOps) 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 | -------------------------------------------------------------------------------- /pkg/history/test/test_hist.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/require" 9 | 10 | amhist "github.com/pancsta/asyncmachine-go/pkg/history" 11 | amss "github.com/pancsta/asyncmachine-go/pkg/states" 12 | ) 13 | 14 | var ss = amss.BasicStates 15 | 16 | func AssertBasics(t *testing.T, mem amhist.MemoryApi, rounds int) { 17 | 18 | ctx := context.Background() 19 | mach := mem.Machine() 20 | start := time.Now() 21 | 22 | // validate 23 | require.True(t, mach.Has(ss.Names()), "Machine has to implement BasicStates") 24 | require.True(t, mem.IsTracked1(ss.Start), "Start should be tracked") 25 | require.False(t, mem.IsTracked1(ss.Ready), "Ready should not be tracked") 26 | 27 | t.Logf("rounds: %d", rounds) 28 | 29 | // mutate 30 | for range rounds { 31 | mach.Toggle1(ss.Start, nil) 32 | } 33 | 34 | t.Logf("mach: %s", time.Since(start)) 35 | 36 | require.NoError(t, mem.Sync()) 37 | 38 | t.Logf("db: %s", time.Since(start)) 39 | 40 | // check conditions 41 | // now := time.Now().UTC() 42 | // require.True(t, mem.ActivatedBetween(ctx, ss.Start, start.UTC(), now), 43 | // "Start was activated") 44 | // require.False(t, mem.ActivatedBetween(ctx, ss.Ready, start.UTC(), now), 45 | // "Ready isn't tracked") 46 | 47 | // machine record 48 | machRec := mem.MachineRecord() 49 | require.NotNil(t, machRec, "Machine record is not nil") 50 | 51 | // many rows, no condition 52 | latest, err := mem.FindLatest(ctx, false, 25, amhist.Query{}) 53 | // sum := []int{} 54 | // for _, r := range latest { 55 | // sum = append(sum, int(r.Time.MTimeSum)) 56 | // } 57 | // print(sum) 58 | require.NoError(t, err) 59 | require.Len(t, latest, 25, "25 rows returned") 60 | require.Equal(t, int(machRec.NextId)-24, int(latest[23].Time.MTimeSum), 61 | "time sum matches") 62 | require.Equal(t, int(machRec.NextId)-25, int(latest[24].Time.MTimeSum), 63 | "time sum matches") 64 | 65 | t.Logf("query: %s", time.Since(start)) 66 | } 67 | 68 | func AssertGc(t *testing.T, mem amhist.MemoryApi, rounds int) { 69 | ctx := context.Background() 70 | 71 | // GC 72 | all, err := mem.FindLatest(ctx, false, mem.Config().MaxRecords*2, 73 | amhist.Query{}) 74 | require.NoError(t, err) 75 | require.LessOrEqual(t, len(all), mem.Config().MaxRecords, 76 | "max records respected") 77 | } 78 | 79 | // TODO test resume 80 | // TODO test schema 81 | // TODO test parallel tracking 82 | // TODO test rpc tracking in /pkg/rpc 83 | // TODO test double tracking 84 | // TODO test config 85 | // TODO test restore 86 | // TODO test transition 87 | // TODO test transition queries 88 | -------------------------------------------------------------------------------- /tools/cmd/am-relay/README.md: -------------------------------------------------------------------------------- 1 | # /tools/cmd/arpc 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 | ## am-relay 9 | 10 | `am-relay` converts formats and relays connections. It's an early version that for now can only rotate [dbg telemetry](/pkg/telemetry/README.md#dbg) 11 | into chunked file dumps. 12 | 13 | ```bash 14 | .rw-r--r--@ 713k foo 17 Nov 12:19 am-dbg-dump-2025-11-17_12-19-35.gob.br 15 | .rw-r--r--@ 737k foo 17 Nov 12:20 am-dbg-dump-2025-11-17_12-20-02.gob.br 16 | .rw-r--r--@ 749k foo 17 Nov 12:20 am-dbg-dump-2025-11-17_12-20-28.gob.br 17 | ``` 18 | 19 | ## Installation 20 | 21 | - [Download a release binary](https://github.com/pancsta/asyncmachine-go/releases/latest) 22 | - Install `go install github.com/pancsta/asyncmachine-go/tools/cmd/am-relay@latest` 23 | - Run directly `go run github.com/pancsta/asyncmachine-go/tools/cmd/am-relay@latest` 24 | 25 | ## Features 26 | 27 | - rotate dbg telemetry 28 | - TODO websocket-to-tcp 29 | - TODO convert gob to JSON 30 | 31 | `$ am-relay --help` 32 | 33 | ```bash 34 | Usage: am-relay [--debug] [] 35 | 36 | Options: 37 | --debug Enable debugging for asyncmachine 38 | --help, -h display this help and exit 39 | 40 | Commands: 41 | rotate-dbg Rotate dbg protocol with fragmented dump files 42 | ``` 43 | 44 | `$ am-relay rotate-dbg --help` 45 | 46 | ```text 47 | Usage: am-relay rotate-dbg [--listen-addr LISTEN-ADDR] [--fwd-addr FWD-ADDR] [--interval-tx INTERVAL-TX] 48 | [--interval-duration INTERVAL-DURATION] [--output OUTPUT] [--dir DIR] 49 | 50 | Options: 51 | --listen-addr LISTEN-ADDR, -l LISTEN-ADDR 52 | Listen address for RPC server [default: localhost:2732] 53 | --fwd-addr FWD-ADDR, -f FWD-ADDR 54 | Address of an RPC server to forward data to (repeatable) 55 | --interval-tx INTERVAL-TX 56 | Amount of transitions to create a dump file [default: 10000] 57 | --interval-duration INTERVAL-DURATION 58 | Amount of human time to create a dump file [default: 24h] 59 | --output OUTPUT, -o OUTPUT 60 | Output file base name [default: am-dbg-dump] 61 | --dir DIR, -d DIR Output directory [default: .] 62 | 63 | Global options: 64 | --debug Enable debugging for asyncmachine 65 | --help, -h display this help and exit 66 | ``` 67 | 68 | ## monorepo 69 | 70 | [Go back to the monorepo root](/README.md) to continue reading. 71 | -------------------------------------------------------------------------------- /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.SetEnvLogLevel(am.LogOps) 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/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,basic", 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.NewSchemaGenerator(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/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.SemLogger().SetSimple(w.log, logLvl) 62 | w.Mach.SemLogger().SetSimple(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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.SetEnvLogLevel(am.LogOps) 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 | -------------------------------------------------------------------------------- /pkg/telemetry/telemetry.go: -------------------------------------------------------------------------------- 1 | package telemetry 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "regexp" 7 | "strings" 8 | 9 | "github.com/ic2hrmk/promtail" 10 | 11 | ssam "github.com/pancsta/asyncmachine-go/pkg/states" 12 | 13 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 14 | ) 15 | 16 | const ( 17 | EnvService = "AM_SERVICE" 18 | EnvLokiAddr = "AM_LOKI_ADDR" 19 | EnvOtelTrace = "AM_OTEL_TRACE" 20 | EnvOtelTraceTxs = "AM_OTEL_TRACE_TXS" 21 | EnvOtelTraceArgs = "AM_OTEL_TRACE_ARGS" 22 | EnvOtelTraceNoauto = "AM_OTEL_TRACE_NOAUTO" 23 | ) 24 | 25 | func BindLokiLogger(mach am.Api, client promtail.Client) { 26 | labels := map[string]string{ 27 | "asyncmachine_id": mach.Id(), 28 | } 29 | mach.SemLogger().EnableId(false) 30 | 31 | amlog := func(level am.LogLevel, msg string, args ...any) { 32 | if strings.HasPrefix(msg, "[error") { 33 | client.LogfWithLabels(promtail.Error, labels, msg, args...) 34 | } else { 35 | switch level { 36 | 37 | case am.LogChanges: 38 | client.LogfWithLabels(promtail.Info, labels, msg, args...) 39 | case am.LogOps: 40 | client.LogfWithLabels(promtail.Info, labels, msg, args...) 41 | case am.LogDecisions: 42 | client.LogfWithLabels(promtail.Debug, labels, msg, args...) 43 | case am.LogEverything: 44 | client.LogfWithLabels(promtail.Debug, labels, msg, args...) 45 | default: 46 | } 47 | } 48 | } 49 | 50 | mach.SemLogger().SetLogger(amlog) 51 | mach.Log("[bind] loki logger") 52 | } 53 | 54 | // everything else than a-z and _ 55 | var normalizeRegexp = regexp.MustCompile("[^a-z_0-9]+") 56 | 57 | func NormalizeId(id string) string { 58 | return normalizeRegexp.ReplaceAllString(strings.ToLower(id), "_") 59 | } 60 | 61 | // BindLokiEnv bind Loki logger to [mach], based on environment vars: 62 | // - AM_SERVICE (required) 63 | // - AM_LOKI_ADDR (required) 64 | // This tracer is NOT inherited by submachines. 65 | func BindLokiEnv(mach am.Api) error { 66 | service := os.Getenv(EnvService) 67 | addr := os.Getenv(EnvLokiAddr) 68 | if service == "" || addr == "" { 69 | return nil 70 | } 71 | 72 | // init promtail and bind AM logger 73 | identifiers := map[string]string{ 74 | "service_name": NormalizeId(service), 75 | } 76 | pt, err := promtail.NewJSONv1Client(addr, identifiers) 77 | if err != nil { 78 | return err 79 | } 80 | 81 | // flush and close 82 | var dispose am.HandlerDispose = func(id string, _ context.Context) { 83 | pt.Close() 84 | } 85 | 86 | // dispose somehow 87 | register := ssam.DisposedStates.RegisterDisposal 88 | if mach.Has1(register) { 89 | mach.Add1(register, am.A{ 90 | ssam.DisposedArgHandler: dispose, 91 | }) 92 | } else { 93 | func() { 94 | <-mach.WhenDisposed() 95 | pt.Close() 96 | }() 97 | } 98 | 99 | BindLokiLogger(mach, pt) 100 | 101 | return nil 102 | } 103 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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}} -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.71.0 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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /pkg/states/ss_disposed.go: -------------------------------------------------------------------------------- 1 | package states 2 | 3 | import ( 4 | "fmt" 5 | 6 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 7 | ) 8 | 9 | // DisposedStatesDef contains all the states of the Disposed state machine. 10 | // Required states: 11 | // - Start 12 | type DisposedStatesDef struct { 13 | *am.StatesBase 14 | 15 | // RegisterDisposal registers a disposal handler passed under the 16 | // DisposedArgHandler key. 17 | RegisterDisposal string 18 | // Disposing indicates that the machine is during the disposal process. 19 | Disposing string 20 | // Disposed indicates that the machine has disposed allocated resoruces 21 | // and is ready to be garbage collected by calling [am.Machine.Dispose]. 22 | Disposed string 23 | } 24 | 25 | // DisposedGroupsDef contains all the state groups Disposed state machine. 26 | type DisposedGroupsDef struct { 27 | Disposed S 28 | } 29 | 30 | // DisposedSchema represents all relations and properties of DisposedStates. 31 | var DisposedSchema = am.Schema{ 32 | ssD.RegisterDisposal: {Multi: true}, 33 | ssD.Disposing: {Remove: sgD.Disposed}, 34 | ssD.Disposed: {Remove: SAdd(sgD.Disposed, S{ssB.Start})}, 35 | } 36 | 37 | // EXPORTS AND GROUPS 38 | 39 | var ( 40 | ssD = am.NewStates(DisposedStatesDef{}) 41 | sgD = am.NewStateGroups(DisposedGroupsDef{ 42 | Disposed: S{ssD.RegisterDisposal, ssD.Disposing, ssD.Disposed}, 43 | }) 44 | 45 | // DisposedStates contains all the states for the Disposed machine. 46 | DisposedStates = ssD 47 | // DisposedGroups contains all the state groups for the Disposed machine. 48 | DisposedGroups = sgD 49 | ) 50 | 51 | // handlers 52 | 53 | // DisposedArgHandler is the key for the disposal handler passed to the 54 | // RegisterDisposal state. It needs to contain the EXPLICIT type of 55 | // am.HandlerDispose, eg 56 | // 57 | // var dispose am.HandlerDispose = func(id string, ctx *am.StateCtx) { 58 | // // ... 59 | // } 60 | var DisposedArgHandler = "DisposedArgHandler" 61 | 62 | type DisposedHandlers struct { 63 | // DisposedHandlers is a list of handler for pkg/states.DisposedStates 64 | DisposedHandlers []am.HandlerDispose 65 | } 66 | 67 | func (h *DisposedHandlers) RegisterDisposalEnter(e *am.Event) bool { 68 | fn, ok := e.Args[DisposedArgHandler].(am.HandlerDispose) 69 | ret := ok && fn != nil 70 | // avoid errs on check mutations 71 | if !ret && !e.IsCheck { 72 | err := fmt.Errorf("%w: DisposedArgHandler invalid", am.ErrInvalidArgs) 73 | e.Machine().AddErr(err, nil) 74 | } 75 | 76 | return ret 77 | } 78 | 79 | func (h *DisposedHandlers) RegisterDisposalState(e *am.Event) { 80 | // TODO ability to deregister a disposal handler (by ref) 81 | fn := e.Args[DisposedArgHandler].(am.HandlerDispose) 82 | h.DisposedHandlers = append(h.DisposedHandlers, fn) 83 | } 84 | 85 | // DisposingState triggers a disposal procedure, but does NOT dispose the 86 | // machine. 87 | func (h *DisposedHandlers) DisposingState(e *am.Event) { 88 | mach := e.Machine() 89 | ctx := mach.NewStateCtx(ssD.Disposing) 90 | 91 | // unblock 92 | go func() { 93 | for _, fn := range h.DisposedHandlers { 94 | if ctx.Err() != nil { 95 | return // expired 96 | } 97 | fn(mach.Id(), ctx) 98 | } 99 | 100 | mach.Add1(ssD.Disposed, nil) 101 | }() 102 | } 103 | -------------------------------------------------------------------------------- /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.SetEnvLogLevel(am.LogOps) 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 | -------------------------------------------------------------------------------- /pkg/history/bbolt/bbolt_test.go: -------------------------------------------------------------------------------- 1 | package bbolt 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | 8 | amhelp "github.com/pancsta/asyncmachine-go/pkg/helpers" 9 | "github.com/stretchr/testify/require" 10 | "go.etcd.io/bbolt" 11 | 12 | amhist "github.com/pancsta/asyncmachine-go/pkg/history" 13 | testhist "github.com/pancsta/asyncmachine-go/pkg/history/test" 14 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 15 | amss "github.com/pancsta/asyncmachine-go/pkg/states" 16 | ) 17 | 18 | var ss = amss.BasicStates 19 | 20 | func TestBboltRead(t *testing.T) { 21 | if amhelp.IsTestRunner() { 22 | t.Skip("skipping debug test") 23 | } 24 | 25 | // init memory 26 | db, err := NewDb("") 27 | require.NoError(t, err) 28 | 29 | // list 30 | // require.NoError(t, db.View(func(tx *bbolt.Tx) error { 31 | // b := tx.Bucket([]byte("MyMach1")) 32 | // bTime := b.Bucket([]byte(BuckTimes)) 33 | // c := bTime.Cursor() 34 | // for k, v := c.First(); k != nil; k, v = c.Next() { 35 | // t.Logf("key: %s, value: %s", k, v) 36 | // } 37 | // return nil 38 | // })) 39 | 40 | // single 41 | require.NoError(t, db.View(func(tx *bbolt.Tx) error { 42 | b := tx.Bucket([]byte("MyMach1")) 43 | bTime := b.Bucket([]byte(BuckTimes)) 44 | t.Logf("key: %d, value: %s", 5, bTime.Get(itob(5))) 45 | return nil 46 | })) 47 | } 48 | 49 | func TestBboltTrack(t *testing.T) { 50 | os.Remove("amhist.db") 51 | debug := false 52 | // debug := true 53 | 54 | // configs 55 | rounds := 50 56 | onErr := func(err error) { 57 | t.Error(err) 58 | } 59 | cfg := Config{ 60 | QueueBatch: 10, 61 | BaseConfig: amhist.BaseConfig{ 62 | Log: debug, 63 | TrackedStates: am.S{ss.Start}, 64 | MaxRecords: 30, 65 | }, 66 | EncJson: debug, 67 | } 68 | 69 | // init basic machine 70 | ctx := context.Background() 71 | mach := am.New(ctx, amss.BasicSchema, &am.Opts{Id: "MyMach1"}) 72 | 73 | // init memory 74 | db, err := NewDb("") 75 | require.NoError(t, err) 76 | defer func() { 77 | t.Logf("write time: %v", db.Stats().TxStats.WriteTime) 78 | }() 79 | 80 | mem, err := NewMemory(ctx, db, mach, cfg, onErr) 81 | require.NoError(t, err) 82 | defer func() { 83 | require.NoError(t, mem.Dispose()) 84 | }() 85 | 86 | // common test 87 | testhist.AssertBasics(t, mem, rounds) 88 | 89 | // GC test 90 | mem.checkGc() 91 | testhist.AssertGc(t, mem, rounds) 92 | } 93 | 94 | func TestBboltTrackMany(t *testing.T) { 95 | os.Remove("amhist.db") 96 | debug := false 97 | // debug := true 98 | 99 | // configs 100 | rounds := 50000 101 | onErr := func(err error) { 102 | t.Error(err) 103 | } 104 | cfg := Config{ 105 | BaseConfig: amhist.BaseConfig{ 106 | Log: debug, 107 | TrackedStates: am.S{ss.Start}, 108 | MaxRecords: 10e6, 109 | }, 110 | EncJson: debug, 111 | } 112 | 113 | // init basic machine 114 | ctx := context.Background() 115 | mach := am.New(ctx, amss.BasicSchema, &am.Opts{Id: "MyMach1"}) 116 | 117 | // init memory 118 | db, err := NewDb("") 119 | require.NoError(t, err) 120 | defer func() { 121 | t.Logf("write time: %v", db.Stats().TxStats.WriteTime) 122 | }() 123 | 124 | mem, err := NewMemory(ctx, db, mach, cfg, onErr) 125 | require.NoError(t, err) 126 | defer func() { 127 | require.NoError(t, mem.Dispose()) 128 | }() 129 | 130 | // common test 131 | testhist.AssertBasics(t, mem, rounds) 132 | } 133 | -------------------------------------------------------------------------------- /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.SetEnvLogLevel(am.LogOps) 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/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?raw=true) 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`](/examples/tree_state_source/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 BasicStatesDef 42 | *ssam.BasicStatesDef 43 | // inherit from rpc/WorkerStatesDef 44 | *ssrpc.WorkerStatesDef 45 | } 46 | ``` 47 | 48 | ## Actors 49 | 50 | - root 51 | - replicant L1 52 | - replicant L2 53 | 54 | Scenario: 55 | 56 | - root process runs the flight-status state machine 57 | - replicants of the 1st level connect to the root 58 | - replicants of the 2st level connect to replicants of the 1st level 59 | - state clocks flow from the root to replicants 60 | - http clients GET active states from the 2nd level replicants 61 | - all state machines are connected to am-dbg, prometheus, and loki 62 | 63 | ## Running 64 | 65 | From the monorepo root: 66 | 67 | 1. `task web-metrics` 68 | 2. `task gen-grafana-tree-state-source` 69 | 3. `task start` 70 | 4. [http://localhost:3000](http://localhost:3000) -> Dashboards -> tree-state-source 71 | 72 | ## Possibilities 73 | 74 | - L1 and L2 replicants can execute handlers locally (TODO) 75 | - root-L1 can have shorter push interval than L1-L2 76 | 77 | ## Benchmark 78 | 79 | 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) 80 | for more info. 81 | 82 | ## Config 83 | 84 | Config available in the `.env` file. 85 | 86 | ```bash 87 | # grafana sync 88 | 89 | GRAFANA_TOKEN= 90 | GRAFANA_URL=http://localhost:3000 91 | 92 | # metrics 93 | 94 | PUSH_GATEWAY_URL=http://localhost:9091 95 | LOKI_ADDR=localhost:3100 96 | AM_LOG=3 97 | 98 | # AM debugging 99 | 100 | #AM_DEBUG=1 101 | #AM_DBG_ADDR=:6831 102 | #AM_HEALTHCHECK=1 103 | 104 | #AM_RPC_LOG_SERVER=1 105 | #AM_RPC_LOG_CLIENT=1 106 | #AM_RPC_LOG_MUX=1 107 | 108 | #AM_NODE_LOG_SUPERVISOR=1 109 | #AM_NODE_LOG_CLIENT=1 110 | #AM_NODE_LOG_WORKER=1 111 | ``` 112 | 113 | ## monorepo 114 | 115 | [Go back to the monorepo root](/README.md) to continue reading. 116 | -------------------------------------------------------------------------------- /tools/debugger/types/dbg_types.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | am "github.com/pancsta/asyncmachine-go/pkg/machine" 11 | ) 12 | 13 | type MachAddress struct { 14 | MachId string 15 | TxId string 16 | MachTime uint64 17 | HumanTime time.Time 18 | // TODO support step 19 | Step int 20 | // TODO support queue ticks 21 | QueueTick uint64 22 | } 23 | 24 | type MachTime struct { 25 | Id string 26 | Time uint64 27 | } 28 | 29 | func (a *MachAddress) Clone() *MachAddress { 30 | return &MachAddress{ 31 | MachId: a.MachId, 32 | TxId: a.TxId, 33 | Step: a.Step, 34 | MachTime: a.MachTime, 35 | } 36 | } 37 | 38 | func (a *MachAddress) String() string { 39 | if a.TxId != "" { 40 | u := fmt.Sprintf("mach://%s/%s", a.MachId, a.TxId) 41 | if a.Step != 0 { 42 | u += fmt.Sprintf("/%d", a.Step) 43 | } 44 | if a.MachTime != 0 { 45 | u += fmt.Sprintf("/t%d", a.MachTime) 46 | } 47 | 48 | return u 49 | } 50 | if a.MachTime != 0 { 51 | return fmt.Sprintf("mach://%s/t%d", a.MachId, a.MachTime) 52 | } 53 | if !a.HumanTime.IsZero() { 54 | return fmt.Sprintf("mach://%s/%s", a.MachId, a.HumanTime) 55 | } 56 | 57 | return fmt.Sprintf("mach://%s", a.MachId) 58 | } 59 | 60 | func ParseMachUrl(u string) (*MachAddress, error) { 61 | // TODO merge parsing with addr bar 62 | 63 | up, err := url.Parse(u) 64 | if err != nil { 65 | return nil, err 66 | } else if up.Host == "" { 67 | return nil, fmt.Errorf("host missing in: %s", u) 68 | } 69 | 70 | addr := &MachAddress{ 71 | MachId: up.Host, 72 | } 73 | p := strings.Split(up.Path, "/") 74 | if len(p) > 1 { 75 | addr.TxId = p[1] 76 | } 77 | if len(p) > 2 { 78 | if s, err := strconv.Atoi(p[2]); err == nil { 79 | addr.Step = s 80 | } 81 | } 82 | 83 | return addr, nil 84 | } 85 | 86 | type MsgTxParsed struct { 87 | StatesAdded []int 88 | StatesRemoved []int 89 | StatesTouched []int 90 | // TimeSum is machine time after this transition. 91 | TimeSum uint64 92 | TimeDiff uint64 93 | ReaderEntries []*LogReaderEntryPtr 94 | // Transitions which reported this one as their source 95 | Forks []MachAddress 96 | ForksLabels []string 97 | // QueueTick when this tx should be executed 98 | ResultTick uint64 99 | } 100 | 101 | type MsgSchemaParsed struct { 102 | Groups map[string]am.S 103 | GroupsOrder []string 104 | } 105 | 106 | type LogReaderEntry struct { 107 | Kind LogReaderKind 108 | // states are empty for logReaderWhenQueue 109 | States []int 110 | // CreatedAt is machine time when this entry was created 111 | CreatedAt uint64 112 | // ClosedAt is human time when this entry was closed, so it can be disposed. 113 | ClosedAt time.Time 114 | 115 | // per-type fields 116 | 117 | // Pipe is for logReaderPipeIn, logReaderPipeOut 118 | Pipe am.MutationType 119 | // Mach is for logReaderPipeIn, logReaderPipeOut, logReaderMention 120 | Mach string 121 | // Ticks is for logReaderWhenTime only 122 | Ticks am.Time 123 | // Args is for logReaderWhenArgs only 124 | Args string 125 | // QueueTick is for logReaderWhenQueue only 126 | QueueTick int 127 | } 128 | 129 | type LogReaderEntryPtr struct { 130 | TxId string 131 | EntryIdx int 132 | } 133 | 134 | type LogReaderKind int 135 | 136 | const ( 137 | LogReaderCtx LogReaderKind = iota + 1 138 | LogReaderWhen 139 | LogReaderWhenNot 140 | LogReaderWhenTime 141 | LogReaderWhenArgs 142 | LogReaderWhenQueue 143 | LogReaderPipeIn 144 | LogReaderPipeOut 145 | // TODO mentions of machine IDs 146 | // logReaderMention 147 | ) 148 | -------------------------------------------------------------------------------- /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.StateException}, 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.2 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.StateException}, 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 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # .goreleaser.yml 2 | # TODO 3 | # • only version: 2 configuration files are supported, yours is version: 0, please update your configuration 4 | # • DEPRECATED: snapshot.name_template should not be used anymore, check https://goreleaser.com/deprecations#snapshotname_template for more info 5 | # • DEPRECATED: archives.format should not be used anymore, check https://goreleaser.com/deprecations#archivesformat for more info 6 | # • DEPRECATED: archives.builds should not be used anymore, check https://goreleaser.com/deprecations#archivesbuilds for more info 7 | 8 | builds: 9 | - 10 | id: "am-dbg" 11 | env: 12 | - CGO_ENABLED=0 13 | - GO111MODULE=on 14 | goos: 15 | - linux 16 | - darwin 17 | - windows 18 | goarch: 19 | - amd64 20 | - arm64 21 | main: ./tools/cmd/am-dbg 22 | binary: am-dbg 23 | 24 | - 25 | id: "am-dbg-ssh" 26 | env: 27 | - CGO_ENABLED=0 28 | - GO111MODULE=on 29 | goos: 30 | - linux 31 | - darwin 32 | - windows 33 | goarch: 34 | - amd64 35 | - arm64 36 | main: ./tools/cmd/am-dbg-ssh 37 | binary: am-dbg-ssh 38 | 39 | - 40 | id: "arpc" 41 | env: 42 | - CGO_ENABLED=0 43 | - GO111MODULE=on 44 | goos: 45 | - linux 46 | - darwin 47 | - windows 48 | goarch: 49 | - amd64 50 | - arm64 51 | main: ./tools/cmd/arpc 52 | binary: arpc 53 | 54 | - 55 | id: "am-relay" 56 | env: 57 | - CGO_ENABLED=0 58 | - GO111MODULE=on 59 | goos: 60 | - linux 61 | - darwin 62 | - windows 63 | goarch: 64 | - amd64 65 | - arm64 66 | main: ./tools/cmd/am-relay 67 | binary: am-relay 68 | 69 | - 70 | id: "am-vis" 71 | env: 72 | - CGO_ENABLED=0 73 | - GO111MODULE=on 74 | goos: 75 | - linux 76 | - darwin 77 | - windows 78 | goarch: 79 | - amd64 80 | - arm64 81 | main: ./tools/cmd/am-vis 82 | binary: am-vis 83 | 84 | archives: 85 | - 86 | id: "am-dbg" 87 | format: tar.gz 88 | name_template: "am-dbg_{{ .Version }}_{{ .Os }}_{{ .Arch }}" 89 | # dont add readmes etc 90 | files: [""] 91 | builds: 92 | - am-dbg 93 | 94 | - 95 | id: "am-dbg-ssh" 96 | format: tar.gz 97 | name_template: "am-dbg-ssh_{{ .Version }}_{{ .Os }}_{{ .Arch }}" 98 | # dont add readmes etc 99 | files: [""] 100 | builds: 101 | - am-dbg 102 | - am-dbg-ssh 103 | 104 | - 105 | id: "arpc" 106 | format: tar.gz 107 | name_template: "arpc_{{ .Version }}_{{ .Os }}_{{ .Arch }}" 108 | # dont add readmes etc 109 | files: [""] 110 | builds: 111 | - arpc 112 | 113 | - 114 | id: "am-relay" 115 | format: tar.gz 116 | name_template: "am-relay_{{ .Version }}_{{ .Os }}_{{ .Arch }}" 117 | # dont add readmes etc 118 | files: [""] 119 | builds: 120 | - am-relay 121 | 122 | - 123 | id: "am-vis" 124 | format: tar.gz 125 | name_template: "am-vis_{{ .Version }}_{{ .Os }}_{{ .Arch }}" 126 | # dont add readmes etc 127 | files: [""] 128 | builds: 129 | - am-vis 130 | 131 | checksum: 132 | name_template: 'checksums.txt' 133 | 134 | release: 135 | github: 136 | owner: pancsta 137 | name: asyncmachine-go 138 | draft: true 139 | replace_existing_draft: true 140 | 141 | changelog: 142 | sort: asc 143 | filters: 144 | exclude: 145 | - '^docs:' 146 | - '^test:' 147 | - '^chore:' 148 | - '^refactor:' 149 | - '^style:' 150 | - '^ci:' 151 | - '^perf:' 152 | - '^revert:' 153 | snapshot: 154 | name_template: "{{ .Tag }}-next" --------------------------------------------------------------------------------