diff --git a/.gitignore b/.gitignore
index 49f7c8d..f07cd88 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,6 +45,10 @@ go.work
# local dev
/.dev
/dist
-/docs/cookbook.md
-/tools/am-dbg/am-dbg
+/tools/cmd/am-dbg/am-dbg
/*.log
+/*.gob
+/assets/demo.cast.yml
+
+_py
+/.run
\ No newline at end of file
diff --git a/.goreleaser.yml b/.goreleaser.yml
index 8174e74..7713431 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -13,7 +13,7 @@ builds:
goarch:
- amd64
- arm64
- main: ./tools/am-dbg/main.go
+ main: ./tools/cmd/am-dbg/main.go
binary: am-dbg
archives:
diff --git a/.mdl_style.rb b/.mdl_style.rb
deleted file mode 100644
index 9636442..0000000
--- a/.mdl_style.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-all
-rule 'MD013', :line_length => 120
-rule 'MD029', :style => :ordered
\ No newline at end of file
diff --git a/README.md b/README.md
index 2af2e3e..6315d7d 100644
--- a/README.md
+++ b/README.md
@@ -1,244 +1,604 @@
# asyncmachine-go
-`asyncmachine-go` is a minimal implementation of [AsyncMachine](https://github.com/TobiaszCudnik/asyncmachine) in
+![TUI Debugger](assets/am-dbg.gif)
+
+**asyncmachine-go** is a minimal implementation of [AsyncMachine](https://github.com/TobiaszCudnik/asyncmachine) in
Golang using **channels and context**. It aims at simplicity and speed.
It can be used as a lightweight in-memory [Temporal](https://github.com/temporalio/temporal) alternative, worker for
-[Asynq](https://github.com/hibiken/asynq), or to write simple consensus engines, stateful firewalls, telemetry, bots,
+[Asynq](https://github.com/hibiken/asynq), or to create simple consensus engines, stateful firewalls, telemetry, bots,
etc.
-AsyncMachine is a relational state machine which never blocks.
-
-```go
-package main
+> **asyncmachine-go** is a general purpose state machine for managing complex asynchronous workflows in a safe and
+> structured way.
-import (
- "context"
- "fmt"
+
- am "github.com/pancsta/asyncmachine-go/pkg/machine"
-)
+See am-dbg's states structure and relations
-func main() {
- ctx := context.Background()
- m := am.New(ctx, am.States{
- "Foo": {
- Add: am.S{"Bar"}
- },
- "Bar": {},
- }, nil)
- m.Add(am.S{"Foo"}, nil)
- fmt.Printf("%s", m) // (Foo:1 Bar:1)
-}
-```
+```go
+var States = am.Struct{
-## Examples
+ ///// Input events
-- [Expense Workflow](/examples/temporal-expense/expense_test.go) \|
- [Temporal version](https://github.com/temporalio/samples-go/blob/main/expense/) \| [Go playground](https://play.golang.com/p/P1eg6tKh6E4)
+ ClientMsg: {Multi: true},
+ ConnectEvent: {Multi: true},
+ DisconnectEvent: {Multi: true},
-```go
-am.States{
- // CreateExpenseActivity
- "CreatingExpense": {
- Remove: am.S{"ExpenseCreated"},
+ // user scrolling tx / steps
+ UserFwd: {
+ Add: S{Fwd},
+ Remove: GroupPlaying,
},
- "ExpenseCreated": {
- Remove: am.S{"CreatingExpense"},
+ UserBack: {
+ Add: S{Back},
+ Remove: GroupPlaying,
},
- // WaitForDecisionActivity
- "WaitingForApproval": {
- Auto: true,
- Require: am.S{"ExpenseCreated"},
- Remove: am.S{"ApprovalGranted"},
+ UserFwdStep: {
+ Add: S{FwdStep},
+ Require: S{ClientSelected},
+ Remove: am.SMerge(GroupPlaying, S{LogUserScrolled}),
},
- "ApprovalGranted": {
- Require: am.S{"ExpenseCreated"},
- Remove: am.S{"WaitingForApproval"},
+ UserBackStep: {
+ Add: S{BackStep},
+ Require: S{ClientSelected},
+ Remove: am.SMerge(GroupPlaying, S{LogUserScrolled}),
},
- // PaymentActivity
- "PaymentInProgress": {
- Auto: true,
- Require: am.S{"ApprovalGranted"},
- Remove: am.S{"PaymentCompleted"},
- },
- "PaymentCompleted": {
- Require: am.S{"ExpenseCreated", "ApprovalGranted"},
- Remove: am.S{"PaymentInProgress"},
+
+ ///// External state (eg UI)
+
+ TreeFocused: {Remove: GroupFocused},
+ LogFocused: {Remove: GroupFocused},
+ SidebarFocused: {Remove: GroupFocused},
+ TimelineTxsFocused: {Remove: GroupFocused},
+ TimelineStepsFocused: {Remove: GroupFocused},
+ MatrixFocused: {Remove: GroupFocused},
+ DialogFocused: {Remove: GroupFocused},
+ StateNameSelected: {Require: S{ClientSelected}},
+ HelpDialog: {Remove: GroupDialog},
+ ExportDialog: {
+ Require: S{ClientSelected},
+ Remove: GroupDialog,
},
-}
-```
+ LogUserScrolled: {},
+ Ready: {Require: S{Start}},
-- [FileProcessing workflow](/examples/temporal-fileprocessing/fileprocessing.go) \|
- [Temporal version](https://github.com/temporalio/samples-go/blob/main/fileprocessing/) \| [Go playground](https://play.golang.com/p/Fv92Xpzlzv6)
+ ///// Actions
-```go
-am.States{
- // DownloadFileActivity
- "DownloadingFile": {
- Remove: am.S{"FileDownloaded"},
+ Start: {},
+ TreeLogView: {
+ Auto: true,
+ Remove: GroupViews,
+ },
+ MatrixView: {Remove: GroupViews},
+ TreeMatrixView: {Remove: GroupViews},
+ TailMode: {
+ Require: S{ClientSelected},
+ Remove: GroupPlaying,
},
- "FileDownloaded": {
- Remove: am.S{"DownloadingFile"},
+ Playing: {
+ Require: S{ClientSelected},
+ Remove: am.SMerge(GroupPlaying, S{LogUserScrolled}),
},
- // ProcessFileActivity
- "ProcessingFile": {
+ Paused: {
Auto: true,
- Require: am.S{"FileDownloaded"},
- Remove: am.S{"FileProcessed"},
+ Require: S{ClientSelected},
+ Remove: GroupPlaying,
},
- "FileProcessed": {
- Remove: am.S{"ProcessingFile"},
+
+ // tx / steps back / fwd
+ Fwd: {
+ Require: S{ClientSelected},
+ Remove: S{Playing},
},
- // UploadFileActivity
- "UploadingFile": {
- Auto: true,
- Require: am.S{"FileProcessed"},
- Remove: am.S{"FileUploaded"},
+ Back: {
+ Require: S{ClientSelected},
+ Remove: S{Playing},
},
- "FileUploaded": {
- Remove: am.S{"UploadingFile"},
+ FwdStep: {
+ Require: S{ClientSelected},
+ Remove: S{Playing},
+ },
+ BackStep: {
+ Require: S{ClientSelected},
+ Remove: S{Playing},
},
-}
-```
-- [FileProcessing in an Asynq worker](examples/asynq-fileprocessing/fileprocessing_task.go)
+ ScrollToTx: {Require: S{ClientSelected}},
-```go
-func HandleFileProcessingTask(ctx context.Context, t *asynq.Task) error {
- var p FileProcessingPayload
- if err := json.Unmarshal(t.Payload(), &p); err != nil {
- return fmt.Errorf("json.Unmarshal failed: %v: %w", err, asynq.SkipRetry)
- }
- log.Printf("Processing file %s", p.Filename)
- // use the FileProcessing workflow ported from Temporal
- machine, err := processor.FileProcessingFlow(ctx, log.Printf, p.Filename)
- if err != nil {
- return err
- }
- // save the machine state as the result
- ret := machine.String()
- if _, err := t.ResultWriter().Write([]byte(ret)); err != nil {
- return fmt.Errorf("failed to write task result: %v", err)
- }
- return nil
+ // client
+ SelectingClient: {Remove: S{ClientSelected}},
+ ClientSelected: {
+ Remove: S{SelectingClient, LogUserScrolled},
+ },
+ RemoveClient: {Require: S{ClientSelected}},
}
```
+
+
+## Comparison
+
+Common differences with other state machines:
+
+- many states can be active at the same time
+- transitions between all the states are allowed
+ - unless constrained
+- states are connected by relations
+- every mutation can be rejected
+- error is a state
+
## Usage
-- Declarative
+### Basics
```go
-// randomly reject the mutation
+package main
+
+import (
+ "context"
+
+ am "github.com/pancsta/asyncmachine-go/pkg/machine"
+)
+
+func main() {
+ ctx := context.Background()
+ mach := am.New(ctx, am.Struct{
+ "ProcessingFile": {
+ Add: am.S{"InProgress"},
+ Remove: am.S{"FileProcessed"},
+ },
+ "FileProcessed": {
+ Remove: am.S{"ProcessingFile", "InProgress"},
+ },
+ "InProgress": {},
+ }, nil)
+ mach.BindHandlers(&Handlers{
+ Filename: "README.md",
+ })
+ // change the state
+ mach.Add1("ProcessingFile", nil)
+ // wait for completed
+ select {
+ case <-time.After(5 * time.Second):
+ println("timeout")
+ case <-mach.WhenErr(nil):
+ println("err:", mach.Err)
+ case <-mach.When1("FileProcessed", nil):
+ println("done")
+ }
+}
+
+type Handlers struct {
+ Filename string
+}
+
+// negotiation handler
func (h *Handlers) ProcessingFileEnter(e *am.Event) bool {
- rand.Seed(time.Now().UnixNano())
- randomBool := rand.Intn(2) == 0
- return randomBool
+ // read-only ops (decide if moving fwd is ok)
+ // lock-free critical zone
+ return true
}
-// cause side effects if accepted
+
+// final handler
func (h *Handlers) ProcessingFileState(e *am.Event) {
+ // read & write ops (but no blocking)
+ // lock-free critical zone
mach := e.Machine
- stateCtx := mach.GetStateCtx("ProcessingFile")
+ // tick-based context
+ stateCtx := mach.NewStateCtx("ProcessingFile")
go func() {
+ // block in the background, locks needed
if stateCtx.Err() != nil {
- return
+ return // expired
}
- err := processFile(h.DownloadedName, stateCtx)
+ // blocking call
+ err := processFile(h.Filename, stateCtx)
if err != nil {
mach.AddErr(err)
return
}
- mach.Add("FileProcessed", nil)
+ // re-check the ctx after a blocking call
+ if stateCtx.Err() != nil {
+ return // expired
+ }
+ // move to the next state in the flow
+ mach.Add1("FileProcessed", nil)
+ }()
+}
+```
+
+### Waiting
+
+```go
+// wait until FileDownloaded becomes active
+<-mach.When1("FileDownloaded", nil)
+
+// wait until FileDownloaded becomes inactive
+<-mach.WhenNot1("DownloadingFile", args, nil)
+
+// wait for EventConnected to be activated with an arg ID=123
+<-mach.WhenArgs("EventConnected", am.A{"ID": 123}, nil)
+
+// wait for Foo to have a tick >= 6 and Bar tick >= 10
+<-mach.WhenTime(am.S{"Foo", "Bar"}, am.T{6, 10}, nil)
+
+// wait for DownloadingFile to have a tick >= 6
+<-mach.WhenTickEq("DownloadingFile", 6, nil)
+
+// wait for DownloadingFile to have a tick increased by 2 since now
+<-mach.WhenTick("DownloadingFile", 2, nil)
+```
+
+See [docs/cookbook.md](docs/cookbook.md) for more snippets.
+
+## Buzzwords
+
+> **AM technically is:** event emitter, queue, dependency graph, AOP, logical clocks, ~2.5k LoC, no deps
+
+> **Flow constraints are:** state mutations, negotiation, relations, "when" methods, state contexts, external contexts
+
+> **AM gives you:** states, events, thread-safety, logging, metrics, traces, debugger, history, flow constraints, scheduler
+
+## Examples
+
+### [FSM - Finite State Machine](/examples/fsm/fsm_test.go)
+
+- [origin](https://en.wikipedia.org/wiki/Finite-state_machine)
+
+
+
+States structure
+
+```go
+var (
+ states = am.Struct{
+ // input states
+ InputPush: {},
+ InputCoin: {},
+
+ // "state" states
+ Locked: {
+ Auto: true,
+ Remove: groupUnlocked,
+ },
+ Unlocked: {Remove: groupUnlocked},
+ }
+)
+```
+
+
+
+### [NFA - Nondeterministic Finite Automaton](/examples/nfa/nfa_test.go)
+
+- [origin](https://en.wikipedia.org/wiki/Nondeterministic_finite_automaton)
+
+
+
+States structure
+
+```go
+var (
+ states = am.Struct{
+ // input states
+ Input: {Multi: true},
+
+ // action states
+ Start: {Add: am.S{StepX}},
+
+ // "state" states
+ StepX: {Remove: groupSteps},
+ Step0: {Remove: groupSteps},
+ Step1: {Remove: groupSteps},
+ Step2: {Remove: groupSteps},
+ Step3: {Remove: groupSteps},
}
+)
+```
+
+
+
+### [PathWatcher](/examples/watcher/watcher.go)
+
+- [origin](https://github.com/pancsta/sway-yast/)
+
+
+
+States structure
+
+```go
+// States map defines relations and properties of states (for files).
+var States = am.Struct{
+ Init: {Add: S{Watching}},
+
+ Watching: {
+ Add: S{Init},
+ After: S{Init},
+ },
+ ChangeEvent: {
+ Multi: true,
+ Require: S{Watching},
+ },
+
+ Refreshing: {
+ Multi: true,
+ Remove: S{AllRefreshed},
+ },
+ Refreshed: {Multi: true},
+ AllRefreshed: {},
+}
+
+// StatesDir map defines relations and properties of states (for directories).
+var StatesDir = am.Struct{
+ Refreshing: {Remove: groupRefreshed},
+ Refreshed: {Remove: groupRefreshed},
+ DirDebounced: {Remove: groupRefreshed},
+ DirCached: {},
}
```
-- Imperative
+
+
+### [Temporal Expense Workflow](/examples/temporal-expense/expense_test.go)
+
+- [origin](https://github.com/temporalio/samples-go/blob/main/expense/)
+
+
+
+States structure
```go
-// change the state
-mach.Add(am.S{"DownloadingFile"},
- am.A{"filename": filename})
-// wait for completed
-select {
-case <-time.After(5 * time.Second):
- return mach, errors.New("timeout")
-case <-mach.WhenErr(nil):
- return mach, mach.Err
-case <-mach.When(am.S{"FileUploaded"}, nil):
+// States map defines relations and properties of states.
+var States = am.Struct{
+ CreatingExpense: {Remove: GroupExpense},
+ ExpenseCreated: {Remove: GroupExpense},
+ WaitingForApproval: {
+ Auto: true,
+ Remove: GroupApproval,
+ },
+ ApprovalGranted: {Remove: GroupApproval},
+ PaymentInProgress: {
+ Auto: true,
+ Remove: GroupPayment,
+ },
+ PaymentCompleted: {Remove: GroupPayment},
}
+
```
-## Documentation
+
-- [API godoc](https://godoc.org/github.com/pancsta/asyncmachine-go/pkg/machine)
-- [Manual](/docs/manual.md)
- - [States](/docs/manual.md#states)
- - [State Clocks](/docs/manual.md#state-clocks)
- - [Auto States](/docs/manual.md#auto-states)
- - [Transitions](/docs/manual.md#transitions)
- - [Negotiation](/docs/manual.md#negotiation-handlers)
- - [Relations](/docs/manual.md#relations)
- - [Queue](/docs/manual.md#queue)
+### [Temporal FileProcessing workflow](/examples/temporal-fileprocessing/fileprocessing.go)
-## TUI Debugger
+- [origin](https://github.com/temporalio/samples-go/blob/main/fileprocessing/)
-`am-dbg` is a simple, yet effective tool to debug your machines, including:
+
-- states with relations
-- time travel
-- transition steps
-- logs
+States structure
-![TUI Debugger](assets/am-dbg.png)
+```go
+// States map defines relations and properties of states.
+var States = am.Struct{
+ DownloadingFile: {Remove: GroupFileDownloaded},
+ FileDownloaded: {Remove: GroupFileDownloaded},
+ ProcessingFile: {
+ Auto: true,
+ Require: S{FileDownloaded},
+ Remove: GroupFileProcessed,
+ },
+ FileProcessed: {Remove: GroupFileProcessed},
+ UploadingFile: {
+ Auto: true,
+ Require: S{FileProcessed},
+ Remove: GroupFileUploaded,
+ },
+ FileUploaded: {Remove: GroupFileUploaded},
+}
+```
-### Installing `am-dbg`
+
+
+### [AM as an Asynq worker](examples/asynq-fileprocessing/fileprocessing_task.go)
+
+## Documentation
-[Download a release binary](https://github.com/pancsta/asyncmachine-go/releases/latest) or use `go install`:
+- [godoc](https://godoc.org/github.com/pancsta/asyncmachine-go/pkg/machine)
+- [cookbook](/docs/cookbook.md)
+- [manual](/docs/manual.md) (outdated)
+ - [States](/docs/manual.md#states)
+ - [State Clocks](/docs/manual.md#state-clocks)
+ - [Auto States](/docs/manual.md#auto-states)
+ - [Transitions](/docs/manual.md#transitions)
+ - [Negotiation](/docs/manual.md#negotiation-handlers)
+ - [Relations](/docs/manual.md#relations)
+ - [Queue](/docs/manual.md#queue)
-`go install github.com/pancsta/asyncmachine-go/tools/am-dbg@latest`
+## Tools
-### Using `am-dbg`
+### Generator
-Set up telemetry:
+`am-gen` will quickly bootstrap a typesafe states file for you.
+
+`$ am-gen states-file Foo,Bar`
+
+
+
+Example template for Foo and Bar
```go
-import (
- "github.com/pancsta/asyncmachine-go/pkg/telemetry"
+package states
+
+import am "github.com/pancsta/asyncmachine-go/pkg/machine"
+
+// S is a type alias for a list of state names.
+type S = am.S
+
+// States map defines relations and properties of states.
+var States = am.Struct{
+ Foo: {},
+ Bar: {},
+}
+
+// Groups of mutually exclusive states.
+
+//var (
+// GroupPlaying = S{Playing, Paused}
+//)
+
+//#region boilerplate defs
+
+// Names of all the states (pkg enum).
+
+const (
+ Foo = "Foo"
+ Bar = "Bar"
)
-// ...
-err := telemetry.MonitorTransitions(mach, telemetry.RpcHost)
+
+// Names is an ordered list of all the state names.
+var Names = S{
+ Foo,
+ Bar,
+ am.Exception,
+}
+
+//#endregion
```
-Run `am-dbg`:
+
-```text
-Usage:
- am-dbg [flags]
+See [`tools/cmd/am-gen`](tools/cmd/am-gen/README.md) for more info.
-Flags:
- --am-dbg-url string Debug this instance of am-dbg with another one
- --enable-mouse Enable mouse support
- -h, --help help for am-dbg
- --log-file string Log file path (default "am-dbg.log")
- --log-level int Log level, 0-5 (silent-everything)
- --log-machine-id Include machine ID in log messages (default true)
- --server-url string Host and port for the server to listen on (default "localhost:9823")
-```
+### Debugger
-## Changelog
+![TUI Debugger](assets/am-dbg.png)
-Latest release: `v0.3.1`
+`am-dbg` is a multi-client debugger lightweight enough to be kept open in the background while receiving data from >100
+ machines simultaneously (and potentially many more). Some features include:
+
+- states tree
+- log view
+- time travel
+- transition steps
+- import / export
+- matrix view
+
+See [`tools/cmd/am-dbg`](tools/cmd/am-dbg/README.md) for more info.
+
+## Integrations
+
+### Open Telemetry
+
+
+
+[`pkg/telemetry`](pkg/telemetry/README.md) provides [Open Telemetry](https://opentelemetry.io/) integration which exposes
+machine's states and transitions as Otel traces, compatible with [Jaeger](https://www.jaegertracing.io/).
-See [CHANELOG.md](/CHANGELOG.md) for details.
+See [`pkg/telemetry`](pkg/telemetry/README.md) for more info or [import a sample asset](assets/bench-jaeger-3h-10m.traces.json?raw=true).
-## Status
+### Prometheus
-**Beta** - although the ideas behind AsyncMachine have been proven to work, the golang implementation is fairly fresh
-and may suffer from bugs, memory leaks, race conditions and even panics.
-[Please report bugs](https://github.com/pancsta/asyncmachine-go/issues/new).
+
-## TODO
+[`pkg/telemetry/prometheus`](pkg/telemetry/prometheus/README.md) binds to machine's transactions and averages the
+values withing an interval window and exposes various metrics. Combined with [Grafana](https://grafana.com/), it can be
+used to monitor the metrics of you machines.
+
+See [`pkg/telemetry/prometheus`](pkg/telemetry/prometheus/README.md) for more info.
+
+## Case Studies
+
+Several case studies are available to show how to implement various types of machines, measure performance and produce
+a lot of inspectable data.
+
+### libp2p-pubsub benchmark
+
+
+
+- **pubsub host** - eg `ps-17` (20 states)
+ PubSub machine is a simple event loop with Multi states which get responses via arg channels. Heavy use of `Eval`.
+- **discovery** - eg `ps-17-disc` (10 states)
+ Discovery machine is a simple event loop with Multi states and a periodic refresh state.
+- **discovery bootstrap** - eg `ps-17-disc-bf3` (5 states)
+ `BootstrapFlow` is a non-linear flow for topic bootstrapping with some retry logic.
+
+See
+[github.com/pancsta/**go-libp2p-pubsub-benchmark**](https://github.com/pancsta/go-libp2p-pubsub-benchmark/#libp2p-pubsub-benchmark)
+or the [pdf results](https://github.com/pancsta/go-libp2p-pubsub-benchmark/raw/main/assets/bench.pdf) for more info.
+
+### libp2p-pubsub simulator
+
+
+
+- **simulator** `sim` (14 states)
+ Root simulator machine, initializes the network and manipulates it during heartbeats according to frequency
+ definitions. Heavily dependent on state negotiation.
+- **simulator's peer** - eg `sim-p17` (17 states)
+ Handles peer's connections, topics and messages. This machine has a decent amount of relations. Each sim peer has its
+ own pubsub host.
+- **topics** - eg `sim-t-se7ev` (5 states)
+ State-only machine (no handlers, no goroutine). States represent correlations with peer machines.
+
+See
+[github.com/pancsta/**go-libp2p-pubsub-benchmark**](https://github.com/pancsta/go-libp2p-pubsub-benchmark/blob/internal/sim)
+and the pubsub machines for more info.
+
+### am-dbg
+
+am-dbg is a [tview](https://github.com/rivo/tview/) TUI app with a single machine consisting of:
+
+- input events (7 states)
+- external state (11 states)
+- actions (14 states)
+
+This machine features a decent amount of relations within a large number od states and 4 state groups. It's also a good
+example to see how easily an AM-based program can be controller with a script in [tools/cmd/am-dbg-demo](tools/cmd/am-dbg-demo/main.go#L68).
+
+See [tools/debugger/states](tools/debugger/states) for more info.
+
+## Roadmap
+
+- negotiation testers (eg `CanAdd`)
+- helpers for composing networks of machines
+- helpers for queue and history traversal
+- "state-trace" navbar in am-dbg (via `AddFromEv`)
+- go1.22 traces
+- inference
+- optimizations
+- manual updated to a spec
+
+See also [issues](https://github.com/pancsta/asyncmachine-go/issues).
+
+## Changelog
+
+Latest release: `v0.3.1`
-See [issues](https://github.com/pancsta/asyncmachine-go/issues).
+- feat: add version param [\#23](https://github.com/pancsta/asyncmachine-go/pull/23) (@pancsta)
+- feat: complete TUI debugger iteration 3 [\#22](https://github.com/pancsta/asyncmachine-go/pull/22) (@pancsta)
+- feat: TUI debugger iteration 2 [\#21](https://github.com/pancsta/asyncmachine-go/pull/21) (@pancsta)
+- feat: add TUI debugger [\#20](https://github.com/pancsta/asyncmachine-go/pull/20) (@pancsta)
+- feat: add telemetry via net/rpc [\#19](https://github.com/pancsta/asyncmachine-go/pull/19) (@pancsta)
+- feat: add support for state groups for the Remove relation [\#17](https://github.com/pancsta/asyncmachine-go/pull/17) (@pancsta)
+- fix: add more locks [\#16](https://github.com/pancsta/asyncmachine-go/pull/16) (@pancsta)
+- feat: prevent empty remove mutations [\#15](https://github.com/pancsta/asyncmachine-go/pull/15) (@pancsta)
+- feat: add VerifyStates for early state names assert [\#14](https://github.com/pancsta/asyncmachine-go/pull/14) (@pancsta)
+- docs: add debugger readme img [\#13](https://github.com/pancsta/asyncmachine-go/pull/13) (@pancsta)
+- docs: add ToC, cheatsheet [\#12](https://github.com/pancsta/asyncmachine-go/pull/12) (@pancsta)
+- docs: align with the v0.2.0 release [\#11](https://github.com/pancsta/asyncmachine-go/pull/11) (@pancsta)
+
+See [CHANELOG.md](/CHANGELOG.md) for the full list.
diff --git a/Taskfile.yml b/Taskfile.yml
index c1ab80c..3978fb0 100644
--- a/Taskfile.yml
+++ b/Taskfile.yml
@@ -26,23 +26,31 @@ tasks:
- task: build-debugger
debugger:
+ silent: false
+ cmds:
+ - go run tools/cmd/am-dbg/main.go {{.CLI_ARGS}}
+
+ debugger-assets:
+ silent: false
cmds:
- - go run tools/am-dbg/main.go {{.CLI_ARGS}}
+ - |
+ go run tools/cmd/am-dbg/main.go \
+ --import-data assets/am-dbg-dump-sim.gob.bz2 \
+ {{.CLI_ARGS}}
debugger-client:
cmds:
- - go run tools/am-dbg/main.go
- --log-machine-id false
+ - go run tools/cmd/am-dbg/main.go
--log-level 2
--am-dbg-url localhost:9913
debugger-debugger:
cmds:
- - go run tools/am-dbg/main.go
+ - go run tools/cmd/am-dbg/main.go
--log-file am-dbg-dbg.log
- --log-machine-id false
--log-level 2
- --server-url localhost:9913
+ --listen-on localhost:9913
+ --select-connected
test:
cmds:
@@ -59,10 +67,28 @@ tasks:
cloc:
cmds:
+ - printf "\n----- pkg/machine\n\n"
- gocloc pkg/machine --not-match=_test\.go
- gocloc pkg/machine/*_test.go
- - gocloc tools/am-dbg/**/* --not-match=_test\.go
+
+ - printf "\n----- pkg/telemetry\n\n"
+ - gocloc pkg/telemetry --not-match=_test\.go
+ - gocloc pkg/telemetry/*_test.go
+
+ - printf "\n----- pkg/history\n\n"
+ - gocloc pkg/history --not-match=_test\.go
+ - gocloc pkg/history/*_test.go
+
+ - printf "\n----- tools/debugger\n\n"
+ - gocloc tools/debugger --not-match=_test\.go
+
+ - printf "\n----- tools/generator\n\n"
+ - gocloc tools/generator --not-match=_test\.go
+
+ - printf "\n----- examples\n\n"
- gocloc examples
+
+ - printf "\n----- docs\n\n"
- gocloc --include-lang=md docs
format:
@@ -91,16 +117,16 @@ tasks:
lint-md:
cmds:
- - mdl -g -c .mdlrc .
+ - mdl -g -c config/mdlrc .
check-fmt:
silent: false
cmds:
- - test -z $($GOFMT -l examples/**/*.go)
- - test -z $($GOFMT -l pkg/**/*.go)
- - test -z $($GOFMT -l tools/am-dbg/*.go)
- - test -z $(goimports -l -local "github.com/pancsta/asyncmachine-go" pkg/**/*.go)
- - test -z $(goimports -l -local "github.com/pancsta/asyncmachine-go" tools/**/*.go)
+ - test -z "$($GOFMT -l examples/**/*.go)"
+ - test -z "$($GOFMT -l pkg/**/*.go)"
+ - test -z "$($GOFMT -l tools/**/*.go)"
+ - test -z "$(goimports -l -local "github.com/pancsta/asyncmachine-go" pkg/**/*.go)"
+ - test -z "$(goimports -l -local "github.com/pancsta/asyncmachine-go" tools/**/*.go)"
changelog:
cmds:
@@ -116,4 +142,60 @@ tasks:
- go install golang.org/x/tools/cmd/goimports@latest
# TODO switch to nodejs md linter
- gem install mdl
- - gem install github_changelog_generator
\ No newline at end of file
+ - gem install github_changelog_generator
+ - pipx install grafanalib
+
+ install-deps-demo:
+ cmds:
+ - npm install -g terminalizer
+
+ gen-grafana-dashboard:
+ desc: Generate Grafana dashboard for provided machine IDs
+ dir: scripts
+ requires:
+ vars: [IDS]
+ env:
+ IDS: $IDS
+ cmds:
+ - generate-dashboard --output ../assets/grafana-mach-{{.IDS}}.json mach.dashboard.py
+
+ grafana-dashboard-list-panels:
+ desc: List panels and grid positions in a generated dashboard
+ requires:
+ vars: [IDS]
+ cmds:
+ - |
+ jq '.. | objects | select(has("gridPos")) | {title: .title, gridPos: .gridPos}' assets/grafana-mach-{{.IDS}}.json
+
+ gen-states-file:
+ desc: Generate states file for provided state names, eg "task gen-states-file -- Foo,Bar"
+ example: task gen-states-file --state-names "state1,state2,state3"
+ cmds:
+ - go run tools/cmd/am-gen/main.go states-file {{.CLI_ARGS}}
+
+ demo-gif:
+ cmds:
+ - terminalizer render assets/demo.cast.yml -o assets/am-dbg.gif
+ - magick assets/am-dbg.gif -layers optimize assets/am-dbg.gif
+
+ demo-gif-quick:
+ cmds:
+ - terminalizer render assets/demo.cast.yml -o assets/am-dbg.gif -q 30
+
+ demo-record:
+ cmds:
+ - terminalizer record --config config/terminalizer.yml --commnad task demo-run
+
+ demo-run:
+ cmds:
+ - go run tools/cmd/am-dbg-demo/main.go
+
+ demo:
+ desc: Create assets/demo.gif using tools/am-dbg-demo
+ cmds:
+ - task: demo-record
+ - task: demo-gif
+
+ godoc:
+ cmds:
+ - godoc -http=:6060
diff --git a/assets/am-dbg-sim.gob.bz2 b/assets/am-dbg-sim.gob.bz2
new file mode 100644
index 0000000..606fce6
Binary files /dev/null and b/assets/am-dbg-sim.gob.bz2 differ
diff --git a/assets/am-dbg.gif b/assets/am-dbg.gif
new file mode 100644
index 0000000..aec5ec9
Binary files /dev/null and b/assets/am-dbg.gif differ
diff --git a/assets/am-dbg.png b/assets/am-dbg.png
index 380e69d..93954d6 100644
Binary files a/assets/am-dbg.png and b/assets/am-dbg.png differ
diff --git a/assets/bench-jaeger-3h-10m.traces.json b/assets/bench-jaeger-3h-10m.traces.json
new file mode 100644
index 0000000..cd68d9a
--- /dev/null
+++ b/assets/bench-jaeger-3h-10m.traces.json
@@ -0,0 +1 @@
+{"data":[{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spans":[{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa","operationName":"transitions","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"34ea00d39795be20"}],"startTime":1717262705761161,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-0"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7c24cee9b6eba798","operationName":"states","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"34ea00d39795be20"}],"startTime":1717262705761158,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-0"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"3a97521474305bcd","operationName":"PoolTimerState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"3b0bb8a45edbf6c3"}],"startTime":1717262705764475,"duration":41,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PoolTimerState"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1960bdbf49f493cd","operationName":"submachines","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"34ea00d39795be20"}],"startTime":1717262705763721,"duration":2,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-0"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"dd916904b6c223f4","operationName":"states","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b20253bc2207a232"}],"startTime":1717262705763754,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-0-disc"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a47565b8219f873b","operationName":"transitions","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b20253bc2207a232"}],"startTime":1717262705763757,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-0-disc"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"4a3307d5439b7bc1","operationName":"PeersPendingState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"8b5d1a6bdd54b84d"}],"startTime":1717262705764720,"duration":27,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PeersPendingState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9b53458e7d2dea46","operationName":"RefreshingDiscoveriesState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"8956fedc5f65f912"}],"startTime":1717262705764582,"duration":10,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"RefreshingDiscoveriesState"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"8956fedc5f65f912","operationName":"[add] RefreshingDiscoveries","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a47565b8219f873b"}],"startTime":1717262705764559,"duration":42,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"35a77a3178d8eb38a099db62d0e7f5b9"},{"key":"time_before","type":"int64","value":2},{"key":"mutation","type":"string","value":"[add] RefreshingDiscoveries"},{"key":"states_diff","type":"string","value":"+RefreshingDiscoveries"},{"key":"time_after","type":"int64","value":3},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":5},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"3b0bb8a45edbf6c3","operationName":"[add] Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a47565b8219f873b"}],"startTime":1717262705764397,"duration":131,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"76e0ac79d2916716c248965eb6cdd14b"},{"key":"time_before","type":"int64","value":0},{"key":"mutation","type":"string","value":"[add] Start"},{"key":"states_diff","type":"string","value":"+Start +PoolTimer"},{"key":"time_after","type":"int64","value":2},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":7},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"4cf86770a9c0e7f1","operationName":"[remove] RefreshingDiscoveries","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a47565b8219f873b"}],"startTime":1717262705764614,"duration":37,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"0d4026841d30623f62860932c5bff6db"},{"key":"time_before","type":"int64","value":3},{"key":"mutation","type":"string","value":"[remove] RefreshingDiscoveries"},{"key":"states_diff","type":"string","value":"-RefreshingDiscoveries"},{"key":"time_after","type":"int64","value":4},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1b5f55165fe049d1","operationName":"RefreshingDiscoveries","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"dd916904b6c223f4"}],"startTime":1717262705764604,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"feabbb82df7504f5","operationName":"RefreshingDiscoveries","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1b5f55165fe049d1"}],"startTime":1717262705764606,"duration":55,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"35a77a3178d8eb38a099db62d0e7f5b9"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a88e6fa22f296b08","operationName":"Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"dd916904b6c223f4"}],"startTime":1717262705764531,"duration":12,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"65840b13264a546a","operationName":"states","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"dd1da5f0e2a463fd"}],"startTime":1717262705765094,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-1"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"81c88944adc11de1","operationName":"PoolTimer","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"dd916904b6c223f4"}],"startTime":1717262705764546,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"5008318bf76b0663","operationName":"PeersPending","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7c24cee9b6eba798"}],"startTime":1717262705764763,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"8b5d1a6bdd54b84d","operationName":"[add] PeersPending","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705764690,"duration":71,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"f22c90f15c0e2fd2d82ad65dd02eba33"},{"key":"time_before","type":"int64","value":0},{"key":"mutation","type":"string","value":"[add] PeersPending"},{"key":"states_diff","type":"string","value":"+PeersPending"},{"key":"time_after","type":"int64","value":1},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"bdb94c1f42488c5b","operationName":"[remove] PeersPending","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705764777,"duration":20,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"d8f8ee30b09d70050b0ce4d32d737356"},{"key":"time_before","type":"int64","value":1},{"key":"mutation","type":"string","value":"[remove] PeersPending"},{"key":"states_diff","type":"string","value":"-PeersPending"},{"key":"time_after","type":"int64","value":2},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"8a0038d73a4ebf3d","operationName":"[add] AddTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705764818,"duration":115,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"62a5043cc626f96606fb51beeadd0b0d"},{"key":"time_before","type":"int64","value":2},{"key":"mutation","type":"string","value":"[add] AddTopic"},{"key":"states_diff","type":"string","value":"+AddTopic"},{"key":"time_after","type":"int64","value":3},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"2d928ece7546349e","operationName":"[remove] AddTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705764951,"duration":34,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"76d9ba3ea7d75b618904a5cb7007a158"},{"key":"time_before","type":"int64","value":3},{"key":"mutation","type":"string","value":"[remove] AddTopic"},{"key":"states_diff","type":"string","value":"-AddTopic"},{"key":"time_after","type":"int64","value":4},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9ea2ccf42cf65e45","operationName":"AddTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7c24cee9b6eba798"}],"startTime":1717262705764935,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"72f3997d581070ac","operationName":"AddTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9ea2ccf42cf65e45"}],"startTime":1717262705764937,"duration":53,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"62a5043cc626f96606fb51beeadd0b0d"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"82e5ea4c2e22bdde","operationName":"PeersPending","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"5008318bf76b0663"}],"startTime":1717262705764765,"duration":33,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"f22c90f15c0e2fd2d82ad65dd02eba33"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"e0b2a78e9d420df1","operationName":"AddTopicEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"8a0038d73a4ebf3d"}],"startTime":1717262705764873,"duration":17,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"AddTopicEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d8aae6315b37fcc3","operationName":"PeersPending","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"65840b13264a546a"}],"startTime":1717262705766592,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"644251958bfa4ca2","operationName":"AddTopicState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"8a0038d73a4ebf3d"}],"startTime":1717262705764900,"duration":20,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"AddTopicState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537","operationName":"transitions","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"dd1da5f0e2a463fd"}],"startTime":1717262705765098,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-1"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0c3c2af4501c449b","operationName":"AddTopicEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"907c2c720fc4ec1b"}],"startTime":1717262705766647,"duration":5,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"AddTopicEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"6f4a21dcbdc58806","operationName":"submachines","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"dd1da5f0e2a463fd"}],"startTime":1717262705765774,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-1"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"577830c1cf204211","operationName":"[remove] PeersPending","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705766603,"duration":23,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"bf1a134a87d76fd97786af51e7e25f55"},{"key":"time_before","type":"int64","value":1},{"key":"mutation","type":"string","value":"[remove] PeersPending"},{"key":"states_diff","type":"string","value":"-PeersPending"},{"key":"time_after","type":"int64","value":2},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ec292f2ef7abd70d","operationName":"states","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ce8927c488b509c6"}],"startTime":1717262705765796,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-1-disc"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"758b758f1267effa","operationName":"PeersPending","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d8aae6315b37fcc3"}],"startTime":1717262705766594,"duration":33,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"5c64821a27d9b59b21aae4113642c439"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b6f2aaf0f8d7809a","operationName":"transitions","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ce8927c488b509c6"}],"startTime":1717262705765799,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-1-disc"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d3f83a3494db5214","operationName":"RefreshingDiscoveriesState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d8c3a26d550bf0f1"}],"startTime":1717262705766598,"duration":31,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"RefreshingDiscoveriesState"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f291ca5f0579a6e1","operationName":"PoolTimerState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ebbed3b2a9b64327"}],"startTime":1717262705766476,"duration":13,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PoolTimerState"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d8c3a26d550bf0f1","operationName":"[add] RefreshingDiscoveries","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b6f2aaf0f8d7809a"}],"startTime":1717262705766555,"duration":90,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"bd27bf15102e93010e3f49e0ffa63f76"},{"key":"time_before","type":"int64","value":2},{"key":"mutation","type":"string","value":"[add] RefreshingDiscoveries"},{"key":"states_diff","type":"string","value":"+RefreshingDiscoveries"},{"key":"time_after","type":"int64","value":3},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":5},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ebbed3b2a9b64327","operationName":"[add] Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b6f2aaf0f8d7809a"}],"startTime":1717262705766450,"duration":50,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"e340c9b43c97a6feb574a52b57d59336"},{"key":"time_before","type":"int64","value":0},{"key":"mutation","type":"string","value":"[add] Start"},{"key":"states_diff","type":"string","value":"+Start +PoolTimer"},{"key":"time_after","type":"int64","value":2},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":7},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d6cf6e462df163b4","operationName":"RefreshingDiscoveries","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ec292f2ef7abd70d"}],"startTime":1717262705766648,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c17362ec6070fc18","operationName":"Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ec292f2ef7abd70d"}],"startTime":1717262705766502,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"4ee6b985dc6a2d84","operationName":"[remove] RefreshingDiscoveries","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b6f2aaf0f8d7809a"}],"startTime":1717262705766664,"duration":38,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"89f7d39542bb0b8da6c0633c05e5c1cf"},{"key":"time_before","type":"int64","value":3},{"key":"mutation","type":"string","value":"[remove] RefreshingDiscoveries"},{"key":"states_diff","type":"string","value":"-RefreshingDiscoveries"},{"key":"time_after","type":"int64","value":4},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"daea4ce841bf5fef","operationName":"PoolTimer","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ec292f2ef7abd70d"}],"startTime":1717262705766504,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d2d94f868927f9bc","operationName":"AddTopicState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"907c2c720fc4ec1b"}],"startTime":1717262705766660,"duration":11,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"AddTopicState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"5168d79f2a0bcf70","operationName":"PeersPendingState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"40054622fc9a68ef"}],"startTime":1717262705766564,"duration":16,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PeersPendingState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"907c2c720fc4ec1b","operationName":"[add] AddTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705766638,"duration":55,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"a912d35ec16c073e3b56ca0420365b7f"},{"key":"time_before","type":"int64","value":2},{"key":"mutation","type":"string","value":"[add] AddTopic"},{"key":"states_diff","type":"string","value":"+AddTopic"},{"key":"time_after","type":"int64","value":3},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"40054622fc9a68ef","operationName":"[add] PeersPending","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705766545,"duration":46,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"5c64821a27d9b59b21aae4113642c439"},{"key":"time_before","type":"int64","value":0},{"key":"mutation","type":"string","value":"[add] PeersPending"},{"key":"states_diff","type":"string","value":"+PeersPending"},{"key":"time_after","type":"int64","value":1},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"4296ad324dac1eb8","operationName":"AddTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"65840b13264a546a"}],"startTime":1717262705766695,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7724ba36f0fd6c19","operationName":"[remove] AddTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705766708,"duration":24,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"d2513366be8e8abc345c82d0324b926a"},{"key":"time_before","type":"int64","value":3},{"key":"mutation","type":"string","value":"[remove] AddTopic"},{"key":"states_diff","type":"string","value":"-AddTopic"},{"key":"time_after","type":"int64","value":4},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c0c93d24f81e2f07","operationName":"RefreshingDiscoveries","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d6cf6e462df163b4"}],"startTime":1717262705766651,"duration":54,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"bd27bf15102e93010e3f49e0ffa63f76"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"05904d8f4b588196","operationName":"AddTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"4296ad324dac1eb8"}],"startTime":1717262705766697,"duration":37,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"a912d35ec16c073e3b56ca0420365b7f"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"37fb78245b14c8ae","operationName":"states","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"4016d057443c05a5"}],"startTime":1717262705767538,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-2-disc"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"26d8763c9109a7db","operationName":"states","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"98d86777aab009c1"}],"startTime":1717262705766824,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-2"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba","operationName":"transitions","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"98d86777aab009c1"}],"startTime":1717262705766827,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-2"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ef6ee7c2084c661e","operationName":"submachines","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"98d86777aab009c1"}],"startTime":1717262705767517,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-2"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"e157bf88d17495d4","operationName":"PeersPendingState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a5359b5ed90b8681"}],"startTime":1717262705768561,"duration":44,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PeersPendingState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1a6332b4daa47f30","operationName":"transitions","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"4016d057443c05a5"}],"startTime":1717262705767541,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-2-disc"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1126da355574aab5","operationName":"PoolTimerState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"cf3c7af873ffd5c8"}],"startTime":1717262705768225,"duration":25,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PoolTimerState"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"cf3c7af873ffd5c8","operationName":"[add] Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1a6332b4daa47f30"}],"startTime":1717262705768194,"duration":100,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"0452c84fb23d80f1de3558eac3046db4"},{"key":"time_before","type":"int64","value":0},{"key":"mutation","type":"string","value":"[add] Start"},{"key":"states_diff","type":"string","value":"+Start +PoolTimer"},{"key":"time_after","type":"int64","value":2},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":7},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"144037c1afcec3ce","operationName":"Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"37fb78245b14c8ae"}],"startTime":1717262705768297,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"36e3c92ca7e702e1","operationName":"PoolTimer","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"37fb78245b14c8ae"}],"startTime":1717262705768302,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a2d12ad929e9bb5f","operationName":"RefreshingDiscoveriesState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c32d30e25fcbb473"}],"startTime":1717262705768384,"duration":34,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"RefreshingDiscoveriesState"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c32d30e25fcbb473","operationName":"[add] RefreshingDiscoveries","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1a6332b4daa47f30"}],"startTime":1717262705768331,"duration":96,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"aa1ca87ddd3bc3546023c52ea07c0023"},{"key":"time_before","type":"int64","value":2},{"key":"mutation","type":"string","value":"[add] RefreshingDiscoveries"},{"key":"states_diff","type":"string","value":"+RefreshingDiscoveries"},{"key":"time_after","type":"int64","value":3},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":5},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a13cd395318b0a99","operationName":"RefreshingDiscoveries","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"37fb78245b14c8ae"}],"startTime":1717262705768430,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"6263937d5e31cf59","operationName":"[remove] RefreshingDiscoveries","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1a6332b4daa47f30"}],"startTime":1717262705768446,"duration":34,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"e346792bb970ffd3be466c68f17a3a32"},{"key":"time_before","type":"int64","value":3},{"key":"mutation","type":"string","value":"[remove] RefreshingDiscoveries"},{"key":"states_diff","type":"string","value":"-RefreshingDiscoveries"},{"key":"time_after","type":"int64","value":4},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"3298c3ba46aceed2","operationName":"RefreshingDiscoveries","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a13cd395318b0a99"}],"startTime":1717262705768433,"duration":49,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"aa1ca87ddd3bc3546023c52ea07c0023"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a2772d36dc98e930","operationName":"[remove] AddTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705768802,"duration":33,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"bb4e9c84e42ae3f894e7066b77b0a6d9"},{"key":"time_before","type":"int64","value":3},{"key":"mutation","type":"string","value":"[remove] AddTopic"},{"key":"states_diff","type":"string","value":"-AddTopic"},{"key":"time_after","type":"int64","value":4},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a5359b5ed90b8681","operationName":"[add] PeersPending","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705768527,"duration":122,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"dca79d03cc0ec9e0e4500b05246fe12c"},{"key":"time_before","type":"int64","value":0},{"key":"mutation","type":"string","value":"[add] PeersPending"},{"key":"states_diff","type":"string","value":"+PeersPending"},{"key":"time_after","type":"int64","value":1},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"4a172f81d69fde82","operationName":"PeersPending","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"26d8763c9109a7db"}],"startTime":1717262705768654,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9dbf04c01684cf07","operationName":"[remove] PeersPending","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705768677,"duration":30,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"38a782d483c7a3c3d426927d73ec9eab"},{"key":"time_before","type":"int64","value":1},{"key":"mutation","type":"string","value":"[remove] PeersPending"},{"key":"states_diff","type":"string","value":"-PeersPending"},{"key":"time_after","type":"int64","value":2},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"57c22e26f7296810","operationName":"PeersPending","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"4a172f81d69fde82"}],"startTime":1717262705768657,"duration":52,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"dca79d03cc0ec9e0e4500b05246fe12c"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"161a65e00c8d553a","operationName":"AddTopicEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f93df66faa8d65a5"}],"startTime":1717262705768745,"duration":6,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"AddTopicEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"180251f96a8649b6","operationName":"AddTopicState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f93df66faa8d65a5"}],"startTime":1717262705768759,"duration":9,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"AddTopicState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f93df66faa8d65a5","operationName":"[add] AddTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705768731,"duration":53,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"db6ef207d8c765554091cc8bc8d6f660"},{"key":"time_before","type":"int64","value":2},{"key":"mutation","type":"string","value":"[add] AddTopic"},{"key":"states_diff","type":"string","value":"+AddTopic"},{"key":"time_after","type":"int64","value":3},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c3c0bbacaae5daf1","operationName":"AddTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"26d8763c9109a7db"}],"startTime":1717262705768787,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0153f8fcaaab0e89","operationName":"[add] AddSubscription","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705768967,"duration":225,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"7bee399c6fcf0edd771ef97a7bba303b"},{"key":"time_before","type":"int64","value":4},{"key":"mutation","type":"string","value":"[add] AddSubscription"},{"key":"states_diff","type":"string","value":"+AddSubscription"},{"key":"time_after","type":"int64","value":5},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"55640affa20e4d78","operationName":"AddTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c3c0bbacaae5daf1"}],"startTime":1717262705768789,"duration":47,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"db6ef207d8c765554091cc8bc8d6f660"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f31df2b581802b18","operationName":"init","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"fd6161db1d657ad6"}],"startTime":1717262705749790,"duration":19052,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"64e83b935dbaf6dd","operationName":"DiscoveringTopicState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7e7b96a2bdcb66fd"}],"startTime":1717262705768910,"duration":20,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"DiscoveringTopicState"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7e7b96a2bdcb66fd","operationName":"[add] DiscoveringTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b6f2aaf0f8d7809a"}],"startTime":1717262705768874,"duration":68,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"72e8adc78e8033a3a4c043e143c89fb6"},{"key":"time_before","type":"int64","value":4},{"key":"mutation","type":"string","value":"[add] DiscoveringTopic"},{"key":"args.source","type":"string","value":"discover.Discover"},{"key":"states_diff","type":"string","value":"+DiscoveringTopic"},{"key":"time_after","type":"int64","value":5},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"e4afdb254cf6204b","operationName":"DiscoveringTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ec292f2ef7abd70d"}],"startTime":1717262705768944,"duration":2,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"528232293e92500f","operationName":"AddSubscriptionEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0153f8fcaaab0e89"}],"startTime":1717262705768980,"duration":5,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"AddSubscriptionEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"6ed3a417ab8020bf","operationName":"AdvertisingTopicState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f72f20a9579bdbd3"}],"startTime":1717262705769125,"duration":16,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"AdvertisingTopicState"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f72f20a9579bdbd3","operationName":"[add] AdvertisingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b6f2aaf0f8d7809a"}],"startTime":1717262705769081,"duration":75,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"9ebae09ea5be2979a599cd9047b79345"},{"key":"time_before","type":"int64","value":5},{"key":"mutation","type":"string","value":"[add] AdvertisingTopic"},{"key":"args.topic","type":"string","value":"foobar"},{"key":"states_diff","type":"string","value":"+AdvertisingTopic"},{"key":"time_after","type":"int64","value":6},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"50da202aef2e69f5","operationName":"AdvertisingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ec292f2ef7abd70d"}],"startTime":1717262705769160,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b6c9f80df88eb357","operationName":"AddSubscriptionState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0153f8fcaaab0e89"}],"startTime":1717262705768993,"duration":177,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"AddSubscriptionState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"5a4f8970644c11f9","operationName":"[remove] AnnouncingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705769354,"duration":27,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"a4183d05516d85252f4365699e9189db"},{"key":"time_before","type":"int64","value":7},{"key":"mutation","type":"string","value":"[remove] AnnouncingTopic"},{"key":"states_diff","type":"string","value":"-AnnouncingTopic"},{"key":"time_after","type":"int64","value":8},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"e6c1ba9a430eb4c4","operationName":"AddSubscription","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"65840b13264a546a"}],"startTime":1717262705769194,"duration":10,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b313d34777f67161","operationName":"[remove] AddSubscription","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705769217,"duration":40,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"4d7b2ecce1f86b772302b9d65d9ebc90"},{"key":"time_before","type":"int64","value":5},{"key":"mutation","type":"string","value":"[remove] AddSubscription"},{"key":"states_diff","type":"string","value":"-AddSubscription"},{"key":"time_after","type":"int64","value":6},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c67039a4f2aea351","operationName":"AddSubscription","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"e6c1ba9a430eb4c4"}],"startTime":1717262705769206,"duration":52,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"7bee399c6fcf0edd771ef97a7bba303b"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"df6570f1f7df7059","operationName":"AnnouncingTopicEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f3e6c552d61a636b"}],"startTime":1717262705769287,"duration":5,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"AnnouncingTopicEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"52c277f3301d3ff9","operationName":"AnnouncingTopicState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f3e6c552d61a636b"}],"startTime":1717262705769301,"duration":24,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"AnnouncingTopicState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f3e6c552d61a636b","operationName":"[add] AnnouncingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705769269,"duration":71,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"d6dba7641cac79c720d7244d63dc2fe4"},{"key":"time_before","type":"int64","value":6},{"key":"mutation","type":"string","value":"[add] AnnouncingTopic"},{"key":"args.topic","type":"string","value":"foobar"},{"key":"states_diff","type":"string","value":"+AnnouncingTopic"},{"key":"time_after","type":"int64","value":7},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"753ac03cff9eed10","operationName":"AnnouncingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"65840b13264a546a"}],"startTime":1717262705769343,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"6e8b41536d8d50e2","operationName":"AnnouncingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"753ac03cff9eed10"}],"startTime":1717262705769344,"duration":38,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"d6dba7641cac79c720d7244d63dc2fe4"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b30f1ab745a17f02","operationName":"DiscoveringTopicState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"6f71f149b2ae839c"}],"startTime":1717262705769418,"duration":18,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"DiscoveringTopicState"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"dfe06493422883df","operationName":"DiscoveringTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"37fb78245b14c8ae"}],"startTime":1717262705769467,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"6f71f149b2ae839c","operationName":"[add] DiscoveringTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1a6332b4daa47f30"}],"startTime":1717262705769394,"duration":64,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"17f4eff368e03b4dd519b359f62b94a0"},{"key":"time_before","type":"int64","value":4},{"key":"mutation","type":"string","value":"[add] DiscoveringTopic"},{"key":"args.source","type":"string","value":"discover.Discover"},{"key":"states_diff","type":"string","value":"+DiscoveringTopic"},{"key":"time_after","type":"int64","value":5},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"01dc3d8a674ffcbc","operationName":"AddSubscriptionEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"525e1a6438a5893d"}],"startTime":1717262705769517,"duration":11,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"AddSubscriptionEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b48181e315da0d0b","operationName":"DiscoveringTopicExit","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d68930a1ae7832dd"}],"startTime":1717262705769893,"duration":48,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"DiscoveringTopicExit"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"6ac0b1f1aef1bede","operationName":"AdvertisingTopicState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"8b010065c0323553"}],"startTime":1717262705770005,"duration":12,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"AdvertisingTopicState"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"12d58a036392513d","operationName":"AdvertisingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"dd916904b6c223f4"}],"startTime":1717262705771940,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d68930a1ae7832dd","operationName":"[remove] DiscoveringTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b6f2aaf0f8d7809a"}],"startTime":1717262705769532,"duration":492,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"2007d74522d964c90ce9263921e69907"},{"key":"time_before","type":"int64","value":6},{"key":"mutation","type":"string","value":"[remove] DiscoveringTopic"},{"key":"states_diff","type":"string","value":"-DiscoveringTopic"},{"key":"time_after","type":"int64","value":7},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ab63ab3bb5e4bc19","operationName":"DiscoveringTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"e4afdb254cf6204b"}],"startTime":1717262705768948,"duration":1079,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"72e8adc78e8033a3a4c043e143c89fb6"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"8b010065c0323553","operationName":"[add] AdvertisingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1a6332b4daa47f30"}],"startTime":1717262705769607,"duration":431,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"e9bd2184add3e48e3c051f8bea29a726"},{"key":"time_before","type":"int64","value":5},{"key":"mutation","type":"string","value":"[add] AdvertisingTopic"},{"key":"args.topic","type":"string","value":"foobar"},{"key":"states_diff","type":"string","value":"+AdvertisingTopic"},{"key":"time_after","type":"int64","value":6},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"5a9fd4633372f4fc","operationName":"AdvertisingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"37fb78245b14c8ae"}],"startTime":1717262705770042,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9528ed4b063f891a","operationName":"[add] TopicDiscovered","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b6f2aaf0f8d7809a"}],"startTime":1717262705770045,"duration":63,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"ad0ef63e690f4b30e187a0e2040484ed"},{"key":"time_before","type":"int64","value":7},{"key":"mutation","type":"string","value":"[add] TopicDiscovered"},{"key":"args.topic","type":"string","value":"foobar"},{"key":"states_diff","type":"string","value":"+TopicDiscovered"},{"key":"time_after","type":"int64","value":8},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b7f9002d04314828","operationName":"TopicDiscovered","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ec292f2ef7abd70d"}],"startTime":1717262705770112,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"4a1f68aae4d9f1bb","operationName":"TopicAnnouncedState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"af43da9904235116"}],"startTime":1717262705770273,"duration":67,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"TopicAnnouncedState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"af43da9904235116","operationName":"[add] TopicAnnounced","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705770226,"duration":145,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"4f9a05887bbdb6857b17f0e4b96825e0"},{"key":"time_before","type":"int64","value":8},{"key":"mutation","type":"string","value":"[add] TopicAnnounced"},{"key":"args.topic","type":"string","value":"foobar"},{"key":"states_diff","type":"string","value":"+TopicAnnounced"},{"key":"time_after","type":"int64","value":9},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f63a50138414f671","operationName":"TopicAnnounced","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"65840b13264a546a"}],"startTime":1717262705770376,"duration":3,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"448f8bc8ab8feac2","operationName":"[remove] TopicAnnounced","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705770436,"duration":40,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"153d5cc6ed8309ce20abaf7ca64d53f4"},{"key":"time_before","type":"int64","value":9},{"key":"mutation","type":"string","value":"[remove] TopicAnnounced"},{"key":"states_diff","type":"string","value":"-TopicAnnounced"},{"key":"time_after","type":"int64","value":10},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"319076f184a54cc2","operationName":"TopicAnnounced","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f63a50138414f671"}],"startTime":1717262705770387,"duration":91,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"4f9a05887bbdb6857b17f0e4b96825e0"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c0f794d7b30e2dd1","operationName":"DiscoveringTopicExit","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9bfb4e6471b45a31"}],"startTime":1717262705770084,"duration":508,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"DiscoveringTopicExit"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9bfb4e6471b45a31","operationName":"[remove] DiscoveringTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1a6332b4daa47f30"}],"startTime":1717262705770063,"duration":696,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"114bddfc296e4206ca8610bfaacb6f75"},{"key":"time_before","type":"int64","value":6},{"key":"mutation","type":"string","value":"[remove] DiscoveringTopic"},{"key":"states_diff","type":"string","value":"-DiscoveringTopic"},{"key":"time_after","type":"int64","value":7},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b53d4dadff055606","operationName":"DiscoveringTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"dfe06493422883df"}],"startTime":1717262705769469,"duration":1294,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"17f4eff368e03b4dd519b359f62b94a0"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"37fcd7ee36a48b01","operationName":"subscribe","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"fd6161db1d657ad6"}],"startTime":1717262705768854,"duration":2765,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"77af3351e3fc937f","operationName":"DiscoveringTopicState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ee83ef0ea03d3656"}],"startTime":1717262705771702,"duration":9,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"DiscoveringTopicState"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"fd0b3902bb9ec3ee","operationName":"[add] TopicDiscovered","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1a6332b4daa47f30"}],"startTime":1717262705770857,"duration":64,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"a2f2e6ce36e6fd0f824d191661bdd410"},{"key":"time_before","type":"int64","value":7},{"key":"mutation","type":"string","value":"[add] TopicDiscovered"},{"key":"args.topic","type":"string","value":"foobar"},{"key":"states_diff","type":"string","value":"+TopicDiscovered"},{"key":"time_after","type":"int64","value":8},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ee83ef0ea03d3656","operationName":"[add] DiscoveringTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a47565b8219f873b"}],"startTime":1717262705771674,"duration":49,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"0ad1213f0e83e5931073ae31d02ad80e"},{"key":"time_before","type":"int64","value":4},{"key":"mutation","type":"string","value":"[add] DiscoveringTopic"},{"key":"args.source","type":"string","value":"discover.Discover"},{"key":"states_diff","type":"string","value":"+DiscoveringTopic"},{"key":"time_after","type":"int64","value":5},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"6e31468f282c1faa","operationName":"TopicAnnounced","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b7950317d9fe1e51"}],"startTime":1717262705771545,"duration":63,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"1c20e75da72381d2b0cb8b6753e27dd5"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"77c89cdf3370d774","operationName":"[remove] AddSubscription","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705771846,"duration":21,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"076728b74b9fa31e93313c96433b0a71"},{"key":"time_before","type":"int64","value":5},{"key":"mutation","type":"string","value":"[remove] AddSubscription"},{"key":"states_diff","type":"string","value":"-AddSubscription"},{"key":"time_after","type":"int64","value":6},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f898e8ef3aae4b55","operationName":"TopicDiscovered","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"37fb78245b14c8ae"}],"startTime":1717262705770925,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1152e8f8cf89e6da","operationName":"DiscoveringTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"dd916904b6c223f4"}],"startTime":1717262705771726,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c1286ef2f4da5ee1","operationName":"AddSubscriptionState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"525e1a6438a5893d"}],"startTime":1717262705769537,"duration":1733,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"AddSubscriptionState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"468c4b380957de18","operationName":"AnnouncingTopicEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"18adcd0330a3a14e"}],"startTime":1717262705771907,"duration":7,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"AnnouncingTopicEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"2f65873b2bbb47d1","operationName":"DiscoveringTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1152e8f8cf89e6da"}],"startTime":1717262705771729,"duration":132,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"0ad1213f0e83e5931073ae31d02ad80e"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c94e758fc36f842a","operationName":"AdvertisingTopicState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"420b3a4f7858242d"}],"startTime":1717262705771909,"duration":12,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"AdvertisingTopicState"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"3c7dec8c115a5403","operationName":"AnnouncingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"3434a272ad26b44d"}],"startTime":1717262705771417,"duration":44,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"8ad3b626e73d690b3e56646d956441e1"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"525e1a6438a5893d","operationName":"[add] AddSubscription","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705769495,"duration":1795,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"45a1e676f4012391585190fb7f8d4a61"},{"key":"time_before","type":"int64","value":4},{"key":"mutation","type":"string","value":"[add] AddSubscription"},{"key":"states_diff","type":"string","value":"+AddSubscription"},{"key":"time_after","type":"int64","value":5},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"495d46faad48a8f9","operationName":"AnnouncingTopicState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"18adcd0330a3a14e"}],"startTime":1717262705771926,"duration":10,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"AnnouncingTopicState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c7202c1f6764299a","operationName":"[remove] TopicAnnounced","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705771560,"duration":46,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"b4af0ecebc7ff417d47af638c5499489"},{"key":"time_before","type":"int64","value":9},{"key":"mutation","type":"string","value":"[remove] TopicAnnounced"},{"key":"states_diff","type":"string","value":"-TopicAnnounced"},{"key":"time_after","type":"int64","value":10},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"661453febf094670","operationName":"AddSubscription","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"463204292b1cfe3d"}],"startTime":1717262705771829,"duration":41,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"0e7f35130c46c381c93cb8cf82086db5"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"420b3a4f7858242d","operationName":"[add] AdvertisingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a47565b8219f873b"}],"startTime":1717262705771877,"duration":60,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"c64e4ce37139e5e331e751503ffd6579"},{"key":"time_before","type":"int64","value":6},{"key":"mutation","type":"string","value":"[add] AdvertisingTopic"},{"key":"args.topic","type":"string","value":"foobar"},{"key":"states_diff","type":"string","value":"+AdvertisingTopic"},{"key":"time_after","type":"int64","value":7},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b7950317d9fe1e51","operationName":"TopicAnnounced","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"26d8763c9109a7db"}],"startTime":1717262705771542,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"90a0ea16b59803a5","operationName":"AddSubscription","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"26d8763c9109a7db"}],"startTime":1717262705771293,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"07c154004436c37a","operationName":"AddSubscription","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"90a0ea16b59803a5"}],"startTime":1717262705771296,"duration":41,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"45a1e676f4012391585190fb7f8d4a61"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"5c806fd1eef32635","operationName":"[add] AddSubscription","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705771746,"duration":78,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"0e7f35130c46c381c93cb8cf82086db5"},{"key":"time_before","type":"int64","value":4},{"key":"mutation","type":"string","value":"[add] AddSubscription"},{"key":"states_diff","type":"string","value":"+AddSubscription"},{"key":"time_after","type":"int64","value":5},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"3434a272ad26b44d","operationName":"AnnouncingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"26d8763c9109a7db"}],"startTime":1717262705771415,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"aadafbf2d548ab20","operationName":"[remove] AnnouncingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705771433,"duration":26,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"ac0b543d90b03f3b685fc70deab11a26"},{"key":"time_before","type":"int64","value":7},{"key":"mutation","type":"string","value":"[remove] AnnouncingTopic"},{"key":"states_diff","type":"string","value":"-AnnouncingTopic"},{"key":"time_after","type":"int64","value":8},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c68371593e0e1314","operationName":"TopicAnnouncedState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"aa8f6393b87a00f3"}],"startTime":1717262705771516,"duration":10,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"TopicAnnouncedState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"decedaa3c787bc0d","operationName":"[add] AnnouncingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705771350,"duration":62,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"8ad3b626e73d690b3e56646d956441e1"},{"key":"time_before","type":"int64","value":6},{"key":"mutation","type":"string","value":"[add] AnnouncingTopic"},{"key":"args.topic","type":"string","value":"foobar"},{"key":"states_diff","type":"string","value":"+AnnouncingTopic"},{"key":"time_after","type":"int64","value":7},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"02f6f1e723fa893c","operationName":"AnnouncingTopicEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"decedaa3c787bc0d"}],"startTime":1717262705771371,"duration":7,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"AnnouncingTopicEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"98e02135249a6c17","operationName":"AddSubscriptionState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"5c806fd1eef32635"}],"startTime":1717262705771780,"duration":25,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"AddSubscriptionState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"aa8f6393b87a00f3","operationName":"[add] TopicAnnounced","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705771485,"duration":54,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"1c20e75da72381d2b0cb8b6753e27dd5"},{"key":"time_before","type":"int64","value":8},{"key":"mutation","type":"string","value":"[add] TopicAnnounced"},{"key":"args.topic","type":"string","value":"foobar"},{"key":"states_diff","type":"string","value":"+TopicAnnounced"},{"key":"time_after","type":"int64","value":9},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"463204292b1cfe3d","operationName":"AddSubscription","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7c24cee9b6eba798"}],"startTime":1717262705771827,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"544092a77af5625c","operationName":"AddSubscriptionEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"5c806fd1eef32635"}],"startTime":1717262705771761,"duration":8,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"AddSubscriptionEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"30e158009e8f72c4","operationName":"DiscoveringTopicExit","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"37879641b6d0bd56"}],"startTime":1717262705771824,"duration":9,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"DiscoveringTopicExit"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"37879641b6d0bd56","operationName":"[remove] DiscoveringTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a47565b8219f873b"}],"startTime":1717262705771804,"duration":55,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"dcc7f4d5a3f6d9b9e923ce45da62000d"},{"key":"time_before","type":"int64","value":5},{"key":"mutation","type":"string","value":"[remove] DiscoveringTopic"},{"key":"states_diff","type":"string","value":"-DiscoveringTopic"},{"key":"time_after","type":"int64","value":6},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"552b09a7e8b0db54","operationName":"[remove] AddSubscription","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705771310,"duration":25,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"f6cbada0b44de079b996b78c60740827"},{"key":"time_before","type":"int64","value":5},{"key":"mutation","type":"string","value":"[remove] AddSubscription"},{"key":"states_diff","type":"string","value":"-AddSubscription"},{"key":"time_after","type":"int64","value":6},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"8ba6b97a420d06f6","operationName":"AnnouncingTopicState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"decedaa3c787bc0d"}],"startTime":1717262705771387,"duration":12,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"AnnouncingTopicState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"18adcd0330a3a14e","operationName":"[add] AnnouncingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705771892,"duration":63,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"ec5dd1183c36613233d0b0518e1bd24a"},{"key":"time_before","type":"int64","value":6},{"key":"mutation","type":"string","value":"[add] AnnouncingTopic"},{"key":"args.topic","type":"string","value":"foobar"},{"key":"states_diff","type":"string","value":"+AnnouncingTopic"},{"key":"time_after","type":"int64","value":7},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9279422e5f2c3a18","operationName":"[add] TopicDiscovered","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a47565b8219f873b"}],"startTime":1717262705771956,"duration":54,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"90c1cf7b2b22634022c7d60401ea5c76"},{"key":"time_before","type":"int64","value":7},{"key":"mutation","type":"string","value":"[add] TopicDiscovered"},{"key":"args.topic","type":"string","value":"foobar"},{"key":"states_diff","type":"string","value":"+TopicDiscovered"},{"key":"time_after","type":"int64","value":8},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"bfce35c84fe26ea0","operationName":"AnnouncingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7c24cee9b6eba798"}],"startTime":1717262705771957,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"881d141acaa59733","operationName":"submachines","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b20253bc2207a232"}],"startTime":1717262705772229,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-0-disc"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"abff6370a8cb8c29","operationName":"TopicDiscovered","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"dd916904b6c223f4"}],"startTime":1717262705772014,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"dc182fbb6e3de396","operationName":"TopicAnnounced","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7c24cee9b6eba798"}],"startTime":1717262705772462,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a3c13426ff93ef1e","operationName":"[remove] AnnouncingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705771982,"duration":35,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"7f555e9655ef1716d912b87e8c07b354"},{"key":"time_before","type":"int64","value":7},{"key":"mutation","type":"string","value":"[remove] AnnouncingTopic"},{"key":"states_diff","type":"string","value":"-AnnouncingTopic"},{"key":"time_after","type":"int64","value":8},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a79c37d897f3f389","operationName":"states","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"25da6841b1d898d6"}],"startTime":1717262705772277,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-0-disc-bf-1-foobar"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"712de283bfd17625","operationName":"AnnouncingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"bfce35c84fe26ea0"}],"startTime":1717262705771959,"duration":60,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"ec5dd1183c36613233d0b0518e1bd24a"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9572a78dd4603104","operationName":"transitions","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"25da6841b1d898d6"}],"startTime":1717262705772281,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-0-disc-bf-1-foobar"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b1391e046dce85f8","operationName":"ps1.Subscribe","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"fd6161db1d657ad6"}],"startTime":1717262705771640,"duration":383,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"66c9750de718b68e","operationName":"TopicAnnouncedState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"56e3a0c1cd628879"}],"startTime":1717262705772424,"duration":17,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"TopicAnnouncedState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"20d0e61707e51388","operationName":"BootstrappingTopicEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f18d80c1cbaf5025"}],"startTime":1717262705772155,"duration":7,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrappingTopicEnter"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"56e3a0c1cd628879","operationName":"[add] TopicAnnounced","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705772387,"duration":72,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"431134ec78f122c719fe5b5257c785b1"},{"key":"time_before","type":"int64","value":8},{"key":"mutation","type":"string","value":"[add] TopicAnnounced"},{"key":"args.topic","type":"string","value":"foobar"},{"key":"states_diff","type":"string","value":"+TopicAnnounced"},{"key":"time_after","type":"int64","value":9},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0a443294e122a162","operationName":"BootstrapChecking","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a79c37d897f3f389"}],"startTime":1717262705774105,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"157cd55519278351","operationName":"StartState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a7cc1842b00d8dc9"}],"startTime":1717262705773999,"duration":38,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"StartState"},{"key":"emitter","type":"string","value":"bootstrapFlow"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"39726074dc1ab48f","operationName":"BootstrapCheckingState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a7cc1842b00d8dc9"}],"startTime":1717262705774047,"duration":24,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrapCheckingState"},{"key":"emitter","type":"string","value":"bootstrapFlow"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0d80ceab1a185e7c","operationName":"[remove] TopicAnnounced","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705772479,"duration":23,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"067f02b52151870e660ef34b34913f4b"},{"key":"time_before","type":"int64","value":9},{"key":"mutation","type":"string","value":"[remove] TopicAnnounced"},{"key":"states_diff","type":"string","value":"-TopicAnnounced"},{"key":"time_after","type":"int64","value":10},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a7cc1842b00d8dc9","operationName":"[add] Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9572a78dd4603104"}],"startTime":1717262705773917,"duration":174,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"c6fe45f8d796feb66620406cf05b5512"},{"key":"time_before","type":"int64","value":0},{"key":"mutation","type":"string","value":"[add] Start"},{"key":"states_diff","type":"string","value":"+Start +BootstrapChecking"},{"key":"time_after","type":"int64","value":2},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":8},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7a586342422c24ce","operationName":"TopicAnnounced","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"dc182fbb6e3de396"}],"startTime":1717262705772465,"duration":39,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"431134ec78f122c719fe5b5257c785b1"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"59620915b33e4184","operationName":"Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a79c37d897f3f389"}],"startTime":1717262705774094,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"dadb7b0b14eb314d","operationName":"PeersPendingState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"dd993812990e05cd"}],"startTime":1717262705774419,"duration":35,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PeersPendingState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c33ed2a40f7fb9cf","operationName":"DiscoveringTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a79c37d897f3f389"}],"startTime":1717262705774213,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"91d581eb393cad26","operationName":"BootstrappingTopicState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f18d80c1cbaf5025"}],"startTime":1717262705772173,"duration":2049,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrappingTopicState"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"86bfb699932db3e9","operationName":"DiscoveringTopicState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f0aa1fec3ee57997"}],"startTime":1717262705774169,"duration":23,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"DiscoveringTopicState"},{"key":"emitter","type":"string","value":"bootstrapFlow"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f18d80c1cbaf5025","operationName":"[add] BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a47565b8219f873b"}],"startTime":1717262705772130,"duration":2110,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"7932ec7313a1ef9a19520c94015fba01"},{"key":"time_before","type":"int64","value":8},{"key":"mutation","type":"string","value":"[add] BootstrappingTopic"},{"key":"args.topic","type":"string","value":"foobar"},{"key":"states_diff","type":"string","value":"+BootstrappingTopic"},{"key":"time_after","type":"int64","value":9},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f0aa1fec3ee57997","operationName":"[add] DiscoveringTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9572a78dd4603104"}],"startTime":1717262705774123,"duration":85,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"b832b414cee41c7f39481c279bc503f3"},{"key":"time_before","type":"int64","value":2},{"key":"mutation","type":"string","value":"[add] DiscoveringTopic"},{"key":"args.source","type":"string","value":"bootstrapFlow.Boo..."},{"key":"states_diff","type":"string","value":"+DiscoveringTopic -BootstrapChecking"},{"key":"time_after","type":"int64","value":4},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":6},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"16f8c5c868c37c27","operationName":"BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"dd916904b6c223f4"}],"startTime":1717262705774258,"duration":6,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"84381353c88c91b2","operationName":"BootstrapChecking","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0a443294e122a162"}],"startTime":1717262705774107,"duration":103,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"c6fe45f8d796feb66620406cf05b5512"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1addd1ff30239560","operationName":"DiscoveringTopicState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"dbb4e46fafcb9ff3"}],"startTime":1717262705774425,"duration":26,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"DiscoveringTopicState"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"85d4aa81ba7f8926","operationName":"TopicDiscovered","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"abff6370a8cb8c29"}],"startTime":1717262705772017,"duration":2713,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"90c1cf7b2b22634022c7d60401ea5c76"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1d2865f61924f467","operationName":"PeersPending","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"4a172f81d69fde82"}],"startTime":1717262705774498,"duration":84,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"ae9d45f2af99c4d64bba73c33624765c"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"29fec05ddd555e4e","operationName":"DiscoveringTopicExit","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0025a26393e10512"}],"startTime":1717262705774594,"duration":8,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"DiscoveringTopicExit"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"dbb4e46fafcb9ff3","operationName":"[add] DiscoveringTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a47565b8219f873b"}],"startTime":1717262705774286,"duration":201,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"8578a65f60b244ab1e4d686b0ca735da"},{"key":"time_before","type":"int64","value":9},{"key":"mutation","type":"string","value":"[add] DiscoveringTopic"},{"key":"args.source","type":"string","value":"bootstrapFlow.Dis..."},{"key":"states_diff","type":"string","value":"+DiscoveringTopic"},{"key":"time_after","type":"int64","value":10},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0025a26393e10512","operationName":"[remove] DiscoveringTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a47565b8219f873b"}],"startTime":1717262705774560,"duration":93,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"a0e4b642410c078d5dbcbd62dac92aec"},{"key":"time_before","type":"int64","value":10},{"key":"mutation","type":"string","value":"[remove] DiscoveringTopic"},{"key":"states_diff","type":"string","value":"-DiscoveringTopic"},{"key":"time_after","type":"int64","value":11},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"dd993812990e05cd","operationName":"[add] PeersPending","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705774354,"duration":138,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"ae9d45f2af99c4d64bba73c33624765c"},{"key":"time_before","type":"int64","value":10},{"key":"mutation","type":"string","value":"[add] PeersPending"},{"key":"states_diff","type":"string","value":"+PeersPending"},{"key":"time_after","type":"int64","value":11},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"af842d3cb1e99c16","operationName":"DiscoveringTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1152e8f8cf89e6da"}],"startTime":1717262705774494,"duration":161,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"8578a65f60b244ab1e4d686b0ca735da"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"66a78b6da6c43174","operationName":"[remove] PeersPending","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705774523,"duration":56,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"75e2100f7835c160ffdcdcb1bdc44d00"},{"key":"time_before","type":"int64","value":11},{"key":"mutation","type":"string","value":"[remove] PeersPending"},{"key":"states_diff","type":"string","value":"-PeersPending"},{"key":"time_after","type":"int64","value":12},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"45c0146d4de99598","operationName":"[add] TopicDiscovered","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a47565b8219f873b"}],"startTime":1717262705774673,"duration":55,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"dc8ee003f4b8f65c535364cde441580e"},{"key":"time_before","type":"int64","value":11},{"key":"mutation","type":"string","value":"[add] TopicDiscovered"},{"key":"args.topic","type":"string","value":"foobar"},{"key":"states_diff","type":"string","value":"+TopicDiscovered"},{"key":"time_after","type":"int64","value":13},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":1},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"e9217720b61d7e20","operationName":"PeersPendingState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"5898d58ca206569c"}],"startTime":1717262705775471,"duration":12,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PeersPendingState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"62058c9637126991","operationName":"BootstrapDelay","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a79c37d897f3f389"}],"startTime":1717262705774851,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"823fa878bd7d096d","operationName":"[remove] PeersPending","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705774820,"duration":47,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"2194c3e17098fbb07b94a25e7dbd1758"},{"key":"time_before","type":"int64","value":11},{"key":"mutation","type":"string","value":"[remove] PeersPending"},{"key":"states_diff","type":"string","value":"-PeersPending"},{"key":"time_after","type":"int64","value":12},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f12dc4e7f7b4ef38","operationName":"PeersPendingState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"5b54d4863cb106ca"}],"startTime":1717262705774731,"duration":24,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PeersPendingState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"4909048cd4833b11","operationName":"PeersPending","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"5008318bf76b0663"}],"startTime":1717262705774794,"duration":76,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"a8c6999e51f032dc0fda9128ac1dc124"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"5b54d4863cb106ca","operationName":"[add] PeersPending","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705774699,"duration":89,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"a8c6999e51f032dc0fda9128ac1dc124"},{"key":"time_before","type":"int64","value":10},{"key":"mutation","type":"string","value":"[add] PeersPending"},{"key":"states_diff","type":"string","value":"+PeersPending"},{"key":"time_after","type":"int64","value":11},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"5898d58ca206569c","operationName":"[add] PeersPending","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705775449,"duration":48,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"1e8e6478af7388ef610e1658ae967954"},{"key":"time_before","type":"int64","value":12},{"key":"mutation","type":"string","value":"[add] PeersPending"},{"key":"states_diff","type":"string","value":"+PeersPending"},{"key":"time_after","type":"int64","value":13},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"412981c29795abdf","operationName":"BootstrapDelayState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"5725fecb08c1230e"}],"startTime":1717262705774802,"duration":29,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrapDelayState"},{"key":"emitter","type":"string","value":"bootstrapFlow"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"5725fecb08c1230e","operationName":"[add] BootstrapDelay","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9572a78dd4603104"}],"startTime":1717262705774761,"duration":86,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"00aaac9273506dd5489f458e7abef59e"},{"key":"time_before","type":"int64","value":4},{"key":"mutation","type":"string","value":"[add] BootstrapDelay"},{"key":"states_diff","type":"string","value":"+BootstrapDelay -DiscoveringTopic"},{"key":"time_after","type":"int64","value":6},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":6},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"6dc269096ba542cb","operationName":"[remove] PeersPending","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705775514,"duration":22,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"fc22fc93caf96db62ad0967d76458f5b"},{"key":"time_before","type":"int64","value":13},{"key":"mutation","type":"string","value":"[remove] PeersPending"},{"key":"states_diff","type":"string","value":"-PeersPending"},{"key":"time_after","type":"int64","value":14},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"dca3d522c271ec05","operationName":"DiscoveringTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c33ed2a40f7fb9cf"}],"startTime":1717262705774215,"duration":633,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"b832b414cee41c7f39481c279bc503f3"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"5dae625dd8ed14c2","operationName":"PeersPendingState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d3ee2bb72b4f689e"}],"startTime":1717262705775562,"duration":39,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PeersPendingState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9c705386a07ddeac","operationName":"[remove] PeersPending","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705775637,"duration":28,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"d5603243587af61445c0a812db04e3ad"},{"key":"time_before","type":"int64","value":11},{"key":"mutation","type":"string","value":"[remove] PeersPending"},{"key":"states_diff","type":"string","value":"-PeersPending"},{"key":"time_after","type":"int64","value":12},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"badcd892f2fbf75e","operationName":"PeersPending","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"5008318bf76b0663"}],"startTime":1717262705775501,"duration":37,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"1e8e6478af7388ef610e1658ae967954"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d3ee2bb72b4f689e","operationName":"[add] PeersPending","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705775536,"duration":82,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"c87c6b1a5724dfe884164d8c9d4a25f1"},{"key":"time_before","type":"int64","value":10},{"key":"mutation","type":"string","value":"[add] PeersPending"},{"key":"states_diff","type":"string","value":"+PeersPending"},{"key":"time_after","type":"int64","value":11},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"25e7000a58d7a218","operationName":"PeersPending","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"4a172f81d69fde82"}],"startTime":1717262705775667,"duration":43,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"2e14ccf7749cb4772e23d0b7939cdf8e"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a4f9566fc1bc6b7d","operationName":"PeersPendingState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ef24abeb16a09106"}],"startTime":1717262705775638,"duration":13,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PeersPendingState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"e420ba90e9866173","operationName":"PeersPendingState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0726c489f33eb348"}],"startTime":1717262705776098,"duration":17,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PeersPendingState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"50c1b7d3a1b8d314","operationName":"[remove] PeersPending","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705776153,"duration":21,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"a8781cded30f0f9f6866c433c5f11094"},{"key":"time_before","type":"int64","value":13},{"key":"mutation","type":"string","value":"[remove] PeersPending"},{"key":"states_diff","type":"string","value":"-PeersPending"},{"key":"time_after","type":"int64","value":14},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"35829dd77a1077b7","operationName":"PeersPending","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d8aae6315b37fcc3"}],"startTime":1717262705775622,"duration":45,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"c87c6b1a5724dfe884164d8c9d4a25f1"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0726c489f33eb348","operationName":"[add] PeersPending","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705776075,"duration":57,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"967c5ffa28b2b5928ef664dbc3ba7d83"},{"key":"time_before","type":"int64","value":12},{"key":"mutation","type":"string","value":"[add] PeersPending"},{"key":"states_diff","type":"string","value":"+PeersPending"},{"key":"time_after","type":"int64","value":13},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"705111d5adf96166","operationName":"[remove] PeersPending","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705775681,"duration":27,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"46fa8ad998c2c27fb6e46858d3c2d156"},{"key":"time_before","type":"int64","value":13},{"key":"mutation","type":"string","value":"[remove] PeersPending"},{"key":"states_diff","type":"string","value":"-PeersPending"},{"key":"time_after","type":"int64","value":14},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"5f6029e8b6ae57fd","operationName":"[remove] PeerNewStream","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705785411,"duration":31,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"4474f774c2d672316f3f40568ac947ff"},{"key":"time_before","type":"int64","value":15},{"key":"mutation","type":"string","value":"[remove] PeerNewStream"},{"key":"states_diff","type":"string","value":"-PeerNewStream"},{"key":"time_after","type":"int64","value":16},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"4aae197a80734f7d","operationName":"PeerNewStream","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"70dabcde43fab5a9"}],"startTime":1717262705785399,"duration":45,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"11eb28f77422d44dc1b208e96d737b9c"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9360c5e19be5be90","operationName":"PeerNewStream","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"70dabcde43fab5a9"}],"startTime":1717262705785798,"duration":37,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"b74126c7da8e63c6d6428d1e490d245d"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"062ecb01d8b6bbb9","operationName":"PeerNewStreamState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"5130e53976527e6d"}],"startTime":1717262705785769,"duration":9,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PeerNewStreamState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"cd6cb7c071db988c","operationName":"PeersPending","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d8aae6315b37fcc3"}],"startTime":1717262705776137,"duration":38,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"967c5ffa28b2b5928ef664dbc3ba7d83"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"e052eab406114836","operationName":"PeerNewStreamState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"136b59b353a9b687"}],"startTime":1717262705785365,"duration":11,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PeerNewStreamState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b8d5104cd71196fe","operationName":"[add] PeerNewStream","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705785858,"duration":47,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"8b199143fa097983b9f838d575edded4"},{"key":"time_before","type":"int64","value":14},{"key":"mutation","type":"string","value":"[add] PeerNewStream"},{"key":"states_diff","type":"string","value":"+PeerNewStream"},{"key":"time_after","type":"int64","value":15},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"5130e53976527e6d","operationName":"[add] PeerNewStream","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705785749,"duration":44,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"b74126c7da8e63c6d6428d1e490d245d"},{"key":"time_before","type":"int64","value":16},{"key":"mutation","type":"string","value":"[add] PeerNewStream"},{"key":"states_diff","type":"string","value":"+PeerNewStream"},{"key":"time_after","type":"int64","value":17},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c90a7a05d84235af","operationName":"PeerNewStream","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"65840b13264a546a"}],"startTime":1717262705785907,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"40c8ac615850491b","operationName":"PeerNewStreamState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"33f27bdeb638777a"}],"startTime":1717262705785369,"duration":13,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PeerNewStreamState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"73cadeab98e1d8c8","operationName":"[remove] PeerNewStream","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705785810,"duration":23,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"76555568e88f6c4c3fdc7529be0e29f7"},{"key":"time_before","type":"int64","value":17},{"key":"mutation","type":"string","value":"[remove] PeerNewStream"},{"key":"states_diff","type":"string","value":"-PeerNewStream"},{"key":"time_after","type":"int64","value":18},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"5bfe7e318d240458","operationName":"[remove] PeerNewStream","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705785907,"duration":26,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"9798222758a141924ce3c93a1cea030f"},{"key":"time_before","type":"int64","value":17},{"key":"mutation","type":"string","value":"[remove] PeerNewStream"},{"key":"states_diff","type":"string","value":"-PeerNewStream"},{"key":"time_after","type":"int64","value":18},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"33f27bdeb638777a","operationName":"[add] PeerNewStream","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705785341,"duration":54,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"16ac1f38b347c5c7c23134da52b14a99"},{"key":"time_before","type":"int64","value":14},{"key":"mutation","type":"string","value":"[add] PeerNewStream"},{"key":"states_diff","type":"string","value":"+PeerNewStream"},{"key":"time_after","type":"int64","value":15},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"bb2609c71f8b442d","operationName":"PeerNewStreamState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"4c4ee4f3087e7dd8"}],"startTime":1717262705785862,"duration":12,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PeerNewStreamState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"70dabcde43fab5a9","operationName":"PeerNewStream","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7c24cee9b6eba798"}],"startTime":1717262705785395,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"3103b7fb1e587135","operationName":"PeerNewStreamState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b8d5104cd71196fe"}],"startTime":1717262705785880,"duration":10,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PeerNewStreamState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"4c4ee4f3087e7dd8","operationName":"[add] PeerNewStream","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705785838,"duration":52,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"2e9881454a1868d70c92ed0ac09ccdfb"},{"key":"time_before","type":"int64","value":16},{"key":"mutation","type":"string","value":"[add] PeerNewStream"},{"key":"states_diff","type":"string","value":"+PeerNewStream"},{"key":"time_after","type":"int64","value":17},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"e1c473897fb455be","operationName":"PeerNewStream","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"26d8763c9109a7db"}],"startTime":1717262705785398,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"136b59b353a9b687","operationName":"[add] PeerNewStream","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705785322,"duration":70,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"11eb28f77422d44dc1b208e96d737b9c"},{"key":"time_before","type":"int64","value":14},{"key":"mutation","type":"string","value":"[add] PeerNewStream"},{"key":"states_diff","type":"string","value":"+PeerNewStream"},{"key":"time_after","type":"int64","value":15},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"296eeef684dd2497","operationName":"[remove] PeerNewStream","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705785413,"duration":23,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"f4e3a1cd20737075d61e7d28e8b9f8e8"},{"key":"time_before","type":"int64","value":15},{"key":"mutation","type":"string","value":"[remove] PeerNewStream"},{"key":"states_diff","type":"string","value":"-PeerNewStream"},{"key":"time_after","type":"int64","value":16},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"38f29e05b7a89766","operationName":"PeerNewStreamState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1a03ca304ea1dff2"}],"startTime":1717262705786396,"duration":13,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PeerNewStreamState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"5a8a3663070a06b0","operationName":"PeerNewStream","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"e1c473897fb455be"}],"startTime":1717262705785400,"duration":38,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"16ac1f38b347c5c7c23134da52b14a99"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c2e097a7b4ed5a52","operationName":"PeerNewStream","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"e1c473897fb455be"}],"startTime":1717262705785894,"duration":41,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"2e9881454a1868d70c92ed0ac09ccdfb"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1a03ca304ea1dff2","operationName":"[add] PeerNewStream","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705786372,"duration":54,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"cbcad84d9a19fe6f23d893e7eb762920"},{"key":"time_before","type":"int64","value":16},{"key":"mutation","type":"string","value":"[add] PeerNewStream"},{"key":"states_diff","type":"string","value":"+PeerNewStream"},{"key":"time_after","type":"int64","value":17},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c6e92a30a73985b5","operationName":"[remove] PeerNewStream","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705785924,"duration":20,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"542df02eecac4ddf6a2c5ab7815beede"},{"key":"time_before","type":"int64","value":15},{"key":"mutation","type":"string","value":"[remove] PeerNewStream"},{"key":"states_diff","type":"string","value":"-PeerNewStream"},{"key":"time_after","type":"int64","value":16},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ef10cae0ac19ea49","operationName":"[remove] PeerNewStream","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705786443,"duration":27,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"201f779621e81ae00f6d346fdf7fd722"},{"key":"time_before","type":"int64","value":17},{"key":"mutation","type":"string","value":"[remove] PeerNewStream"},{"key":"states_diff","type":"string","value":"-PeerNewStream"},{"key":"time_after","type":"int64","value":18},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"5f37fdfb6a28b2cc","operationName":"PeerNewStream","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c90a7a05d84235af"}],"startTime":1717262705785910,"duration":36,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"8b199143fa097983b9f838d575edded4"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a27681d928e434ab","operationName":"PeerNewStream","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c90a7a05d84235af"}],"startTime":1717262705786430,"duration":43,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"cbcad84d9a19fe6f23d893e7eb762920"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ef24abeb16a09106","operationName":"[add] PeersPending","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705775617,"duration":48,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"2e14ccf7749cb4772e23d0b7939cdf8e"},{"key":"time_before","type":"int64","value":12},{"key":"mutation","type":"string","value":"[add] PeersPending"},{"key":"states_diff","type":"string","value":"+PeersPending"},{"key":"time_after","type":"int64","value":13},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d0b303e9b2508cf4","operationName":"[add] BootstrapChecking","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9572a78dd4603104"}],"startTime":1717262705875804,"duration":163,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"ee4e42b810dfebebb6f3e22ad465cc19"},{"key":"time_before","type":"int64","value":6},{"key":"mutation","type":"string","value":"[add] BootstrapChecking"},{"key":"states_diff","type":"string","value":"+BootstrapChecking -BootstrapDelay"},{"key":"time_after","type":"int64","value":8},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":6},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"005d86573e5633bd","operationName":"BootstrapCheckingState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d0b303e9b2508cf4"}],"startTime":1717262705875893,"duration":48,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrapCheckingState"},{"key":"emitter","type":"string","value":"bootstrapFlow"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"4a0319727053d563","operationName":"TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a79c37d897f3f389"}],"startTime":1717262705876100,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d54ec978c546622e","operationName":"BootstrapDelay","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"62058c9637126991"}],"startTime":1717262705774853,"duration":101116,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"00aaac9273506dd5489f458e7abef59e"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"3f4a3966d949e414","operationName":"[add] TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9572a78dd4603104"}],"startTime":1717262705876000,"duration":92,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"529f5b08d742d4b30f0ad5bc7879a0fe"},{"key":"time_before","type":"int64","value":8},{"key":"mutation","type":"string","value":"[add] TopicBootstrapped"},{"key":"states_diff","type":"string","value":"+TopicBootstrapped -BootstrapChecking"},{"key":"time_after","type":"int64","value":10},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":5},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9f773fd23a9414c1","operationName":"BootstrapChecking","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0a443294e122a162"}],"startTime":1717262705875975,"duration":121,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"ee4e42b810dfebebb6f3e22ad465cc19"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"fcf8d9dbe6eea793","operationName":"PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7c24cee9b6eba798"}],"startTime":1717262705876935,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"987bbd8ea57f2c1a","operationName":"TopicBootstrappedState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"174a097683735a94"}],"startTime":1717262705876258,"duration":35,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"TopicBootstrappedState"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"174a097683735a94","operationName":"[add] TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a47565b8219f873b"}],"startTime":1717262705876176,"duration":162,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"74d15c06a9672733b09655106d87ebd4"},{"key":"time_before","type":"int64","value":13},{"key":"mutation","type":"string","value":"[add] TopicBootstrapped"},{"key":"args.topic","type":"string","value":"foobar"},{"key":"states_diff","type":"string","value":"+TopicBootstrapped"},{"key":"time_after","type":"int64","value":14},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d12d683e8445bad4","operationName":"TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"dd916904b6c223f4"}],"startTime":1717262705876346,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"4f109a8a36a39e63","operationName":"BootstrappingTopicExit","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"cbeaee9eef76010d"}],"startTime":1717262705876484,"duration":23,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrappingTopicExit"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"cbeaee9eef76010d","operationName":"[remove] BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a47565b8219f873b"}],"startTime":1717262705876385,"duration":222,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"611881999c88e6d8e06cce2088acdded"},{"key":"time_before","type":"int64","value":14},{"key":"mutation","type":"string","value":"[remove] BootstrappingTopic"},{"key":"states_diff","type":"string","value":"-BootstrappingTopic"},{"key":"time_after","type":"int64","value":15},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b1956d578d825452","operationName":"BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"16f8c5c868c37c27"}],"startTime":1717262705774266,"duration":102346,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"7932ec7313a1ef9a19520c94015fba01"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"37e4061a5a1758aa","operationName":"Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"59620915b33e4184"}],"startTime":1717262705774097,"duration":102614,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"c6fe45f8d796feb66620406cf05b5512"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"927fd8680362769e","operationName":"TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"4a0319727053d563"}],"startTime":1717262705876104,"duration":611,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"529f5b08d742d4b30f0ad5bc7879a0fe"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"25da6841b1d898d6","operationName":"mach:ps-0-disc-bf-1-foobar","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"881d141acaa59733"}],"startTime":1717262705772238,"duration":104482,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"id","type":"string","value":"ps-0-disc-bf-1-foobar"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"622f325d8da0c896","operationName":"PublishMessageEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"3474959376b4d5e3"}],"startTime":1717262705876801,"duration":32,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b6ff24dfa9388092","operationName":"PublishMessageState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"3474959376b4d5e3"}],"startTime":1717262705876857,"duration":35,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"3474959376b4d5e3","operationName":"[add] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705876746,"duration":183,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"53b2ff86dc672829092be174b4cde9bf"},{"key":"time_before","type":"int64","value":18},{"key":"mutation","type":"string","value":"[add] PublishMessage"},{"key":"states_diff","type":"string","value":"+PublishMessage"},{"key":"time_after","type":"int64","value":19},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"478f5925a21b1913","operationName":"PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"031e417fb7dda017"}],"startTime":1717262705877672,"duration":95,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"b9ee79967a5c7c7fb06ca652287755cc"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a36e7a42628820af","operationName":"[remove] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705876969,"duration":55,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"cccbc097266009e8e69dba705d001fd0"},{"key":"time_before","type":"int64","value":19},{"key":"mutation","type":"string","value":"[remove] PublishMessage"},{"key":"states_diff","type":"string","value":"-PublishMessage"},{"key":"time_after","type":"int64","value":20},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d143a913b632f1c9","operationName":"PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"fcf8d9dbe6eea793"}],"startTime":1717262705876941,"duration":87,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"53b2ff86dc672829092be174b4cde9bf"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"79c9eced67d600c5","operationName":"first message","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"fd6161db1d657ad6"}],"startTime":1717262705772038,"duration":105001,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"2380704139b7bd9b","operationName":"PublishMessageEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"524e97890477452c"}],"startTime":1717262705877498,"duration":32,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"18c7de4501eb166d","operationName":"PublishMessageEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"2b1dd7300ff2b349"}],"startTime":1717262705877526,"duration":23,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"dbfb146dca7b6d6e","operationName":"PublishMessageState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"524e97890477452c"}],"startTime":1717262705877553,"duration":27,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"6d5ef9614639f0af","operationName":"verify first message","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"fd6161db1d657ad6"}],"startTime":1717262705877067,"duration":546,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1bfaf99f2d7dc9eb","operationName":"PublishMessageState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"2b1dd7300ff2b349"}],"startTime":1717262705877579,"duration":34,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"dda743e0e78939a8","operationName":"BootstrappingTopicEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"daa998322f28f3b3"}],"startTime":1717262705877929,"duration":23,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrappingTopicEnter"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"cd741da4d202b261","operationName":"PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"65840b13264a546a"}],"startTime":1717262705877629,"duration":3,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"524e97890477452c","operationName":"[add] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705877451,"duration":171,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"3f5bb8eb435e496d5859b57bb925c095"},{"key":"time_before","type":"int64","value":18},{"key":"mutation","type":"string","value":"[add] PublishMessage"},{"key":"states_diff","type":"string","value":"+PublishMessage"},{"key":"time_after","type":"int64","value":19},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"2b1dd7300ff2b349","operationName":"[add] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705877449,"duration":211,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"b9ee79967a5c7c7fb06ca652287755cc"},{"key":"time_before","type":"int64","value":18},{"key":"mutation","type":"string","value":"[add] PublishMessage"},{"key":"states_diff","type":"string","value":"+PublishMessage"},{"key":"time_after","type":"int64","value":19},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"effe91e5034afbd2","operationName":"states","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"03b05780b27cd85f"}],"startTime":1717262705878122,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-0-disc-bf-3-foobar"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"031e417fb7dda017","operationName":"PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"26d8763c9109a7db"}],"startTime":1717262705877667,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"682da8bb776d6ad8","operationName":"transitions","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"03b05780b27cd85f"}],"startTime":1717262705878133,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-0-disc-bf-3-foobar"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9a57c71e5b54bdca","operationName":"[remove] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705877665,"duration":52,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"b7d963b7b82e95f9ac38e7f5286cb8d5"},{"key":"time_before","type":"int64","value":19},{"key":"mutation","type":"string","value":"[remove] PublishMessage"},{"key":"states_diff","type":"string","value":"-PublishMessage"},{"key":"time_after","type":"int64","value":20},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9b5cad83cb3ddf23","operationName":"StartState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d5603ea7e7f1102c"}],"startTime":1717262705879751,"duration":39,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"StartState"},{"key":"emitter","type":"string","value":"bootstrapFlow"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"dbbdbe25e352cf5d","operationName":"BootstrapCheckingState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d5603ea7e7f1102c"}],"startTime":1717262705879805,"duration":49,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrapCheckingState"},{"key":"emitter","type":"string","value":"bootstrapFlow"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d5603ea7e7f1102c","operationName":"[add] Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"682da8bb776d6ad8"}],"startTime":1717262705879688,"duration":185,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"9327a36c91ac5784eb5875a8f9571055"},{"key":"time_before","type":"int64","value":0},{"key":"mutation","type":"string","value":"[add] Start"},{"key":"states_diff","type":"string","value":"+Start +BootstrapChecking"},{"key":"time_after","type":"int64","value":2},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":8},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"82b288c1b6d3009a","operationName":"Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"effe91e5034afbd2"}],"startTime":1717262705879878,"duration":2,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"effe809191fa17e6","operationName":"BootstrapChecking","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"effe91e5034afbd2"}],"startTime":1717262705879889,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"8ed569a296349e11","operationName":"[add] TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"682da8bb776d6ad8"}],"startTime":1717262705879915,"duration":82,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"0fee41e60962ffbc521929a5720b15e5"},{"key":"time_before","type":"int64","value":2},{"key":"mutation","type":"string","value":"[add] TopicBootstrapped"},{"key":"states_diff","type":"string","value":"+TopicBootstrapped -BootstrapChecking"},{"key":"time_after","type":"int64","value":4},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":5},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"966b3ea5562a1108","operationName":"BootstrapChecking","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"effe809191fa17e6"}],"startTime":1717262705879894,"duration":106,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"9327a36c91ac5784eb5875a8f9571055"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"884837752d52ad46","operationName":"TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"effe91e5034afbd2"}],"startTime":1717262705880004,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"4e4082b8fd6e8e86","operationName":"BootstrappingTopicState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"daa998322f28f3b3"}],"startTime":1717262705878012,"duration":2012,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrappingTopicState"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"daa998322f28f3b3","operationName":"[add] BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a47565b8219f873b"}],"startTime":1717262705877820,"duration":2237,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"d44425629bb02318fbef89c4da762cdf"},{"key":"time_before","type":"int64","value":15},{"key":"mutation","type":"string","value":"[add] BootstrappingTopic"},{"key":"args.topic","type":"string","value":"foobar"},{"key":"states_diff","type":"string","value":"+BootstrappingTopic"},{"key":"time_after","type":"int64","value":16},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9593b056560ce85b","operationName":"TopicBootstrappedState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ff6ebe8b520768d2"}],"startTime":1717262705880187,"duration":28,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"TopicBootstrappedState"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ff6ebe8b520768d2","operationName":"[add] TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a47565b8219f873b"}],"startTime":1717262705880124,"duration":141,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"1423d3868e675089622d9729d543ac13"},{"key":"time_before","type":"int64","value":16},{"key":"mutation","type":"string","value":"[add] TopicBootstrapped"},{"key":"args.topic","type":"string","value":"foobar"},{"key":"states_diff","type":"string","value":"+TopicBootstrapped"},{"key":"time_after","type":"int64","value":18},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f1a29faead93a1be","operationName":"PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"cd741da4d202b261"}],"startTime":1717262705877636,"duration":84,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"3f5bb8eb435e496d5859b57bb925c095"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9fe4c46d4b8ec1eb","operationName":"[remove] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705877703,"duration":60,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"2c79a34b66d437a70513bb17c19c62e7"},{"key":"time_before","type":"int64","value":19},{"key":"mutation","type":"string","value":"[remove] PublishMessage"},{"key":"states_diff","type":"string","value":"-PublishMessage"},{"key":"time_after","type":"int64","value":20},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0dba9c5827fb9cbc","operationName":"BootstrappingTopicExit","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"029d9732ebfd04fa"}],"startTime":1717262705880385,"duration":30,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrappingTopicExit"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"4fd6daae8a4f4387","operationName":"TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d12d683e8445bad4"}],"startTime":1717262705876352,"duration":3919,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"74d15c06a9672733b09655106d87ebd4"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9e21711551d67787","operationName":"BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"16f8c5c868c37c27"}],"startTime":1717262705880066,"duration":406,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"d44425629bb02318fbef89c4da762cdf"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"5703d234bf61bcb3","operationName":"TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"884837752d52ad46"}],"startTime":1717262705880008,"duration":528,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"0fee41e60962ffbc521929a5720b15e5"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a792296d12207531","operationName":"PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"fcf8d9dbe6eea793"}],"startTime":1717262705880980,"duration":123,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"fffd822bd553e401642feede413b16b7"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"03b05780b27cd85f","operationName":"mach:ps-0-disc-bf-3-foobar","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"881d141acaa59733"}],"startTime":1717262705878094,"duration":2447,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"id","type":"string","value":"ps-0-disc-bf-3-foobar"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7af46194d2821cd0","operationName":"PublishMessageEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"117cc8541d524688"}],"startTime":1717262705880816,"duration":19,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"fbaab498b6768c42","operationName":"[remove] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705881037,"duration":62,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"3358ef3f821d67e980d8c9776cc32c99"},{"key":"time_before","type":"int64","value":21},{"key":"mutation","type":"string","value":"[remove] PublishMessage"},{"key":"states_diff","type":"string","value":"-PublishMessage"},{"key":"time_after","type":"int64","value":22},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"e2b30d64a6b68df5","operationName":"PublishMessageEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c6c77747ccbfb632"}],"startTime":1717262705881537,"duration":34,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"e592bd84fc40e97f","operationName":"PublishMessageState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"117cc8541d524688"}],"startTime":1717262705880880,"duration":43,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b72c39036b3ef1f0","operationName":"[remove] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705881823,"duration":97,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"d677ac072be7287644438ef5ba1a1f83"},{"key":"time_before","type":"int64","value":21},{"key":"mutation","type":"string","value":"[remove] PublishMessage"},{"key":"states_diff","type":"string","value":"-PublishMessage"},{"key":"time_after","type":"int64","value":22},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9baa4ccba22ba58c","operationName":"PublishMessageEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1bf9558ff0ca919a"}],"startTime":1717262705881587,"duration":33,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1bf9558ff0ca919a","operationName":"[add] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705881516,"duration":262,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"de86f2b5cb824003bc11ffcb5fa36d23"},{"key":"time_before","type":"int64","value":20},{"key":"mutation","type":"string","value":"[add] PublishMessage"},{"key":"states_diff","type":"string","value":"+PublishMessage"},{"key":"time_after","type":"int64","value":21},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1be62e931d92ea3a","operationName":"PublishMessageState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c6c77747ccbfb632"}],"startTime":1717262705881597,"duration":64,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"546cef4d80d5951a","operationName":"submachines","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"4016d057443c05a5"}],"startTime":1717262705882290,"duration":3,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-2-disc"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"6112c0550140873a","operationName":"PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"031e417fb7dda017"}],"startTime":1717262705881787,"duration":142,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"de86f2b5cb824003bc11ffcb5fa36d23"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"029d9732ebfd04fa","operationName":"[remove] BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a47565b8219f873b"}],"startTime":1717262705880304,"duration":165,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"560c3c95afee117430326b841ff37b82"},{"key":"time_before","type":"int64","value":18},{"key":"mutation","type":"string","value":"[remove] BootstrappingTopic"},{"key":"states_diff","type":"string","value":"-BootstrappingTopic"},{"key":"time_after","type":"int64","value":19},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c4c629dcd10a01e2","operationName":"BootstrappingTopicEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7427812d2cc090a2"}],"startTime":1717262705881996,"duration":24,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrappingTopicEnter"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"fa56ba5a3278199f","operationName":"[remove] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705881789,"duration":84,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"ec6941bc260d6d7eeaf54e57ffbaf4f1"},{"key":"time_before","type":"int64","value":21},{"key":"mutation","type":"string","value":"[remove] PublishMessage"},{"key":"states_diff","type":"string","value":"-PublishMessage"},{"key":"time_after","type":"int64","value":22},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"118335e628c73329","operationName":"PublishMessageState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1bf9558ff0ca919a"}],"startTime":1717262705881663,"duration":31,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"3c061b270c7471cc","operationName":"PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"cd741da4d202b261"}],"startTime":1717262705881750,"duration":128,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"afe1c13e01ac7e31df0b29ccc1cf8431"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"6309145a2bf6513c","operationName":"StartState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"48d9da39e7e37f5a"}],"startTime":1717262705884174,"duration":42,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"StartState"},{"key":"emitter","type":"string","value":"bootstrapFlow"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"48d9da39e7e37f5a","operationName":"[add] Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c994b4c9c97c2593"}],"startTime":1717262705884102,"duration":211,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"e0a8ec8e0c6cf1fc7fcf58073bfe15ef"},{"key":"time_before","type":"int64","value":0},{"key":"mutation","type":"string","value":"[add] Start"},{"key":"states_diff","type":"string","value":"+Start +BootstrapChecking"},{"key":"time_after","type":"int64","value":2},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":8},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"456a623a3a02246c","operationName":"states","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"39da1f31ba50a227"}],"startTime":1717262705882356,"duration":2,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-2-disc-bf-1-foobar"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b0c099c04c3d8553","operationName":"BootstrapCheckingState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"48d9da39e7e37f5a"}],"startTime":1717262705884237,"duration":56,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrapCheckingState"},{"key":"emitter","type":"string","value":"bootstrapFlow"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c994b4c9c97c2593","operationName":"transitions","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"39da1f31ba50a227"}],"startTime":1717262705882362,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-2-disc-bf-1-foobar"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"36d846ce713f7bc1","operationName":"[add] TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c994b4c9c97c2593"}],"startTime":1717262705884353,"duration":121,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"7181f4cae666e396ba895ff6d0b75f05"},{"key":"time_before","type":"int64","value":2},{"key":"mutation","type":"string","value":"[add] TopicBootstrapped"},{"key":"states_diff","type":"string","value":"+TopicBootstrapped -BootstrapChecking"},{"key":"time_after","type":"int64","value":4},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":5},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f15678ab26b5eaf6","operationName":"BootstrapChecking","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7fe183bc35a8384c"}],"startTime":1717262705884335,"duration":150,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"e0a8ec8e0c6cf1fc7fcf58073bfe15ef"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7fe183bc35a8384c","operationName":"BootstrapChecking","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"456a623a3a02246c"}],"startTime":1717262705884331,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c6c77747ccbfb632","operationName":"[add] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705881484,"duration":220,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"afe1c13e01ac7e31df0b29ccc1cf8431"},{"key":"time_before","type":"int64","value":20},{"key":"mutation","type":"string","value":"[add] PublishMessage"},{"key":"states_diff","type":"string","value":"+PublishMessage"},{"key":"time_after","type":"int64","value":21},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"75b5ba1b756ca64e","operationName":"BootstrappingTopicState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7427812d2cc090a2"}],"startTime":1717262705882128,"duration":2383,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrappingTopicState"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7427812d2cc090a2","operationName":"[add] BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1a6332b4daa47f30"}],"startTime":1717262705881903,"duration":2662,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"09e0c04842cdada2c3ad11b955c8aa36"},{"key":"time_before","type":"int64","value":8},{"key":"mutation","type":"string","value":"[add] BootstrappingTopic"},{"key":"args.topic","type":"string","value":"foobar"},{"key":"states_diff","type":"string","value":"+BootstrappingTopic"},{"key":"time_after","type":"int64","value":9},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"117cc8541d524688","operationName":"[add] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705880756,"duration":205,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"fffd822bd553e401642feede413b16b7"},{"key":"time_before","type":"int64","value":20},{"key":"mutation","type":"string","value":"[add] PublishMessage"},{"key":"states_diff","type":"string","value":"+PublishMessage"},{"key":"time_after","type":"int64","value":21},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0990f0cbd850f8a8","operationName":"TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"456a623a3a02246c"}],"startTime":1717262705884489,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"3c790bd6108f4b32","operationName":"Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"82b288c1b6d3009a"}],"startTime":1717262705879885,"duration":653,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"9327a36c91ac5784eb5875a8f9571055"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"bbd73064b45966b1","operationName":"TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0990f0cbd850f8a8"}],"startTime":1717262705884494,"duration":166,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"7181f4cae666e396ba895ff6d0b75f05"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"358405354de30368","operationName":"Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"456a623a3a02246c"}],"startTime":1717262705884320,"duration":3,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"3081c38c58d391ac","operationName":"Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"358405354de30368"}],"startTime":1717262705884328,"duration":343,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"e0a8ec8e0c6cf1fc7fcf58073bfe15ef"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"49604bf5fa2a05d8","operationName":"BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"37fb78245b14c8ae"}],"startTime":1717262705884573,"duration":2,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"39da1f31ba50a227","operationName":"mach:ps-2-disc-bf-1-foobar","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"546cef4d80d5951a"}],"startTime":1717262705882306,"duration":2368,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"id","type":"string","value":"ps-2-disc-bf-1-foobar"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"fd49eba31799c678","operationName":"[remove] BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1a6332b4daa47f30"}],"startTime":1717262705884894,"duration":158,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"88dbd132eb214fe090a446e652c0b206"},{"key":"time_before","type":"int64","value":10},{"key":"mutation","type":"string","value":"[remove] BootstrappingTopic"},{"key":"args.topic","type":"string","value":"foobar"},{"key":"states_diff","type":"string","value":"-BootstrappingTopic"},{"key":"time_after","type":"int64","value":11},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"4875c0973b13d954","operationName":"TopicBootstrappedState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"8d4cb380329d908c"}],"startTime":1717262705884775,"duration":32,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"TopicBootstrappedState"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"8d4cb380329d908c","operationName":"[add] TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1a6332b4daa47f30"}],"startTime":1717262705884695,"duration":145,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"fb3d83be096fa815b403a13b6e6f0846"},{"key":"time_before","type":"int64","value":9},{"key":"mutation","type":"string","value":"[add] TopicBootstrapped"},{"key":"args.topic","type":"string","value":"foobar"},{"key":"states_diff","type":"string","value":"+TopicBootstrapped"},{"key":"time_after","type":"int64","value":10},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a73398ce083e803f","operationName":"TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"37fb78245b14c8ae"}],"startTime":1717262705884845,"duration":15,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c5dffa8d3d195e23","operationName":"BootstrappingTopicExit","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"fd49eba31799c678"}],"startTime":1717262705884964,"duration":26,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrappingTopicExit"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7ae66fad593a7b4b","operationName":"PublishMessageEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f2c79292625aa203"}],"startTime":1717262705885503,"duration":22,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"151d9e594193b229","operationName":"BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"49604bf5fa2a05d8"}],"startTime":1717262705884579,"duration":476,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"09e0c04842cdada2c3ad11b955c8aa36"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b9e1ee3d14dc72cb","operationName":"[remove] BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1a6332b4daa47f30"}],"startTime":1717262705885093,"duration":72,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"6d758c14d035fbfb9299895a358057de"},{"key":"time_before","type":"int64","value":11},{"key":"mutation","type":"string","value":"[remove] BootstrappingTopic"},{"key":"states_diff","type":"string","value":""},{"key":"time_after","type":"int64","value":11},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"5a437a57a9bfd5ad","operationName":"PublishMessageState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f2c79292625aa203"}],"startTime":1717262705885550,"duration":24,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f2c79292625aa203","operationName":"[add] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705885473,"duration":159,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"a924dfe3b6a9d64d5a038c8ed24787d7"},{"key":"time_before","type":"int64","value":22},{"key":"mutation","type":"string","value":"[add] PublishMessage"},{"key":"states_diff","type":"string","value":"+PublishMessage"},{"key":"time_after","type":"int64","value":23},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"114320d335e4a4e7","operationName":"PublishMessageEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e60c1b86e90c2bd"}],"startTime":1717262705886145,"duration":38,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"86cf163d4fc47a77","operationName":"[remove] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705885669,"duration":67,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"d8b106563a58ccb8b62bfb820a930d19"},{"key":"time_before","type":"int64","value":23},{"key":"mutation","type":"string","value":"[remove] PublishMessage"},{"key":"states_diff","type":"string","value":"-PublishMessage"},{"key":"time_after","type":"int64","value":24},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"666177270e11a687","operationName":"[remove] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705886427,"duration":79,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"f22efe3278accf12f720295bd2191189"},{"key":"time_before","type":"int64","value":23},{"key":"mutation","type":"string","value":"[remove] PublishMessage"},{"key":"states_diff","type":"string","value":"-PublishMessage"},{"key":"time_after","type":"int64","value":24},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9365c83471addc6f","operationName":"PublishMessageState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"88842ced9fbe5edf"}],"startTime":1717262705886278,"duration":40,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"63dad39a88818c5b","operationName":"PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"031e417fb7dda017"}],"startTime":1717262705885639,"duration":102,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"a924dfe3b6a9d64d5a038c8ed24787d7"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7bcb60b8cddc77aa","operationName":"[remove] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705886322,"duration":77,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"6bc2584033a960063ed28d2a08bc7a44"},{"key":"time_before","type":"int64","value":23},{"key":"mutation","type":"string","value":"[remove] PublishMessage"},{"key":"states_diff","type":"string","value":"-PublishMessage"},{"key":"time_after","type":"int64","value":24},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"88842ced9fbe5edf","operationName":"[add] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705886170,"duration":205,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"16605e078755274b87b8f20c758f48f5"},{"key":"time_before","type":"int64","value":22},{"key":"mutation","type":"string","value":"[add] PublishMessage"},{"key":"states_diff","type":"string","value":"+PublishMessage"},{"key":"time_after","type":"int64","value":23},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b928f4c1798b7789","operationName":"PublishMessageState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e60c1b86e90c2bd"}],"startTime":1717262705886200,"duration":44,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ae7180844a9362fb","operationName":"PublishMessageEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"88842ced9fbe5edf"}],"startTime":1717262705886225,"duration":29,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"682e26b8bd6e776a","operationName":"PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"cd741da4d202b261"}],"startTime":1717262705886280,"duration":121,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"4c1ec96c64c2cba5a55208b5ab1e6fa2"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"cd6be701f49be288","operationName":"PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"fcf8d9dbe6eea793"}],"startTime":1717262705886383,"duration":127,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"16605e078755274b87b8f20c758f48f5"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e60c1b86e90c2bd","operationName":"[add] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705886065,"duration":210,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"4c1ec96c64c2cba5a55208b5ab1e6fa2"},{"key":"time_before","type":"int64","value":22},{"key":"mutation","type":"string","value":"[add] PublishMessage"},{"key":"states_diff","type":"string","value":"+PublishMessage"},{"key":"time_after","type":"int64","value":23},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7489716b2676e487","operationName":"transitions","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"dfd23e18fa591da2"}],"startTime":1717262705886972,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-1-disc-bf-1-foobar"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f767720fdb297134","operationName":"states","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"dfd23e18fa591da2"}],"startTime":1717262705886964,"duration":2,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-1-disc-bf-1-foobar"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9c219ec43280bdbc","operationName":"[add] Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7489716b2676e487"}],"startTime":1717262705888732,"duration":208,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"83d89a47547f4973a68ac925229ca4a9"},{"key":"time_before","type":"int64","value":0},{"key":"mutation","type":"string","value":"[add] Start"},{"key":"states_diff","type":"string","value":"+Start +BootstrapChecking"},{"key":"time_after","type":"int64","value":2},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":8},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7a59416d7f7c6ab8","operationName":"BootstrappingTopicState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"4b54e03406e33dbb"}],"startTime":1717262705886769,"duration":2311,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrappingTopicState"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"45016d561b5d5001","operationName":"BootstrapChecking","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f767720fdb297134"}],"startTime":1717262705888967,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c548ed4972d9bf4f","operationName":"Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f767720fdb297134"}],"startTime":1717262705888946,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1d2528908f763556","operationName":"StartState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9c219ec43280bdbc"}],"startTime":1717262705888804,"duration":35,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"StartState"},{"key":"emitter","type":"string","value":"bootstrapFlow"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"bba79af499efc66d","operationName":"[add] TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7489716b2676e487"}],"startTime":1717262705888986,"duration":72,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"473f41d43858d641968ff3d4a80e2bb2"},{"key":"time_before","type":"int64","value":2},{"key":"mutation","type":"string","value":"[add] TopicBootstrapped"},{"key":"states_diff","type":"string","value":"+TopicBootstrapped -BootstrapChecking"},{"key":"time_after","type":"int64","value":4},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":5},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"35aac1040bc9a29e","operationName":"BootstrapCheckingState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9c219ec43280bdbc"}],"startTime":1717262705888854,"duration":47,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrapCheckingState"},{"key":"emitter","type":"string","value":"bootstrapFlow"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"cd970d88c298653c","operationName":"BootstrapChecking","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"45016d561b5d5001"}],"startTime":1717262705888970,"duration":90,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"83d89a47547f4973a68ac925229ca4a9"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"bd7007f53f24e132","operationName":"TopicBootstrappedState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d2210d436a92645a"}],"startTime":1717262705889300,"duration":24,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"TopicBootstrappedState"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"dfd23e18fa591da2","operationName":"mach:ps-1-disc-bf-1-foobar","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"8a4e738ccae9bfee"}],"startTime":1717262705886921,"duration":2243,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"id","type":"string","value":"ps-1-disc-bf-1-foobar"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"eb577870e4caaa70","operationName":"TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f767720fdb297134"}],"startTime":1717262705889063,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"710cc66cbbdee99e","operationName":"BootstrappingTopicEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"4b54e03406e33dbb"}],"startTime":1717262705886666,"duration":39,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrappingTopicEnter"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"8a4e738ccae9bfee","operationName":"submachines","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ce8927c488b509c6"}],"startTime":1717262705886907,"duration":3,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-1-disc"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"4b54e03406e33dbb","operationName":"[add] BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b6f2aaf0f8d7809a"}],"startTime":1717262705886594,"duration":2594,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"e02601a8d5ff6b221412d0f7bf6d2dc6"},{"key":"time_before","type":"int64","value":8},{"key":"mutation","type":"string","value":"[add] BootstrappingTopic"},{"key":"args.topic","type":"string","value":"foobar"},{"key":"states_diff","type":"string","value":"+BootstrappingTopic"},{"key":"time_after","type":"int64","value":9},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"3cd8aeeb719e3faa","operationName":"TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ec292f2ef7abd70d"}],"startTime":1717262705889391,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"694795460b5fb11c","operationName":"Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c548ed4972d9bf4f"}],"startTime":1717262705888950,"duration":206,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"83d89a47547f4973a68ac925229ca4a9"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a980c23560376e89","operationName":"BootstrappingTopicExit","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"53343e23d1b8c2d6"}],"startTime":1717262705889514,"duration":34,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrappingTopicExit"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7377dc17849ae3f2","operationName":"BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ec292f2ef7abd70d"}],"startTime":1717262705889195,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"53343e23d1b8c2d6","operationName":"[remove] BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b6f2aaf0f8d7809a"}],"startTime":1717262705889462,"duration":176,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"b06e484bf02a1a6144fa3121459bc544"},{"key":"time_before","type":"int64","value":10},{"key":"mutation","type":"string","value":"[remove] BootstrappingTopic"},{"key":"args.topic","type":"string","value":"foobar"},{"key":"states_diff","type":"string","value":"-BootstrappingTopic"},{"key":"time_after","type":"int64","value":11},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d51b74fea03b8813","operationName":"BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7377dc17849ae3f2"}],"startTime":1717262705889200,"duration":443,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"e02601a8d5ff6b221412d0f7bf6d2dc6"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"2d2d9287ce80fd20","operationName":"[add] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705890024,"duration":591,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"5b10d8e1ccdfa3ca2ede7b864799fe28"},{"key":"time_before","type":"int64","value":24},{"key":"mutation","type":"string","value":"[add] PublishMessage"},{"key":"states_diff","type":"string","value":"+PublishMessage"},{"key":"time_after","type":"int64","value":25},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a65c69b01f9c3d5b","operationName":"PublishMessageEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"2d2d9287ce80fd20"}],"startTime":1717262705890425,"duration":17,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"dd0edb09bb75070f","operationName":"TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"eb577870e4caaa70"}],"startTime":1717262705889066,"duration":94,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"473f41d43858d641968ff3d4a80e2bb2"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b0d2e5788ee77f4c","operationName":"PublishMessageState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"2d2d9287ce80fd20"}],"startTime":1717262705890467,"duration":32,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d976241680dde0b6","operationName":"[remove] BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b6f2aaf0f8d7809a"}],"startTime":1717262705889673,"duration":67,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"83ea854226791e72faece02293a9bb51"},{"key":"time_before","type":"int64","value":11},{"key":"mutation","type":"string","value":"[remove] BootstrappingTopic"},{"key":"states_diff","type":"string","value":""},{"key":"time_after","type":"int64","value":11},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d2210d436a92645a","operationName":"[add] TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b6f2aaf0f8d7809a"}],"startTime":1717262705889227,"duration":157,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"7209476f5487f9fb2699a6c206c44660"},{"key":"time_before","type":"int64","value":9},{"key":"mutation","type":"string","value":"[add] TopicBootstrapped"},{"key":"args.topic","type":"string","value":"foobar"},{"key":"states_diff","type":"string","value":"+TopicBootstrapped"},{"key":"time_after","type":"int64","value":10},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"dc752bba1105ebcc","operationName":"[remove] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705890707,"duration":76,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"d6c96d064e83734bc105bb1bdd89ac96"},{"key":"time_before","type":"int64","value":25},{"key":"mutation","type":"string","value":"[remove] PublishMessage"},{"key":"states_diff","type":"string","value":"-PublishMessage"},{"key":"time_after","type":"int64","value":26},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ef7bbb388f7f0446","operationName":"PublishMessageEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"583d165d2a4fa3c1"}],"startTime":1717262705891620,"duration":15,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"dd314c5302cb5ab7","operationName":"PublishMessageState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"583d165d2a4fa3c1"}],"startTime":1717262705891692,"duration":128,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9a014fb22b8a83f4","operationName":"PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"cd741da4d202b261"}],"startTime":1717262705890677,"duration":109,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"5b10d8e1ccdfa3ca2ede7b864799fe28"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a34e3090e5f12cd1","operationName":"PublishMessageEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9ffd7eaa5a968e2b"}],"startTime":1717262705891904,"duration":24,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"583d165d2a4fa3c1","operationName":"[add] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705891577,"duration":339,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"d5e5a8f52392676d57235f1ecf50d4a2"},{"key":"time_before","type":"int64","value":24},{"key":"mutation","type":"string","value":"[add] PublishMessage"},{"key":"states_diff","type":"string","value":"+PublishMessage"},{"key":"time_after","type":"int64","value":25},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"78c71eb9c7c2828c","operationName":"PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"fcf8d9dbe6eea793"}],"startTime":1717262705891938,"duration":181,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"d5e5a8f52392676d57235f1ecf50d4a2"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"181094e79bed74ee","operationName":"PublishMessageState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9ffd7eaa5a968e2b"}],"startTime":1717262705892010,"duration":496,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"218f1faf55e407bf","operationName":"[remove] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705891998,"duration":114,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"824d379db9d86f3fb6b00433de83535c"},{"key":"time_before","type":"int64","value":25},{"key":"mutation","type":"string","value":"[remove] PublishMessage"},{"key":"states_diff","type":"string","value":"-PublishMessage"},{"key":"time_after","type":"int64","value":26},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9ffd7eaa5a968e2b","operationName":"[add] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705891730,"duration":825,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"8ecb25b767e4782a2531ce8823d26f35"},{"key":"time_before","type":"int64","value":24},{"key":"mutation","type":"string","value":"[add] PublishMessage"},{"key":"states_diff","type":"string","value":"+PublishMessage"},{"key":"time_after","type":"int64","value":25},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9496f7fcb49054b0","operationName":"[remove] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705892603,"duration":61,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"3d997547fa797c125e9c54d74ad6704a"},{"key":"time_before","type":"int64","value":25},{"key":"mutation","type":"string","value":"[remove] PublishMessage"},{"key":"states_diff","type":"string","value":"-PublishMessage"},{"key":"time_after","type":"int64","value":26},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"34aabe3d17c2a9e3","operationName":"Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"cc20bd3db35d545e"}],"startTime":1717262705894805,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"6ac415c6113216ad","operationName":"PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"031e417fb7dda017"}],"startTime":1717262705892564,"duration":104,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"8ecb25b767e4782a2531ce8823d26f35"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b70aa99bfbcc0a1f","operationName":"BootstrappingTopicEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"de41ca440e4295c8"}],"startTime":1717262705892771,"duration":27,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrappingTopicEnter"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"cc20bd3db35d545e","operationName":"states","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"8f54b693e692cd83"}],"startTime":1717262705892945,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-1-disc-bf-3-foobar"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"6ce192201667b3b1","operationName":"transitions","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"8f54b693e692cd83"}],"startTime":1717262705892949,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-1-disc-bf-3-foobar"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0fe354ad906729c6","operationName":"StartState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"6d3258ba9b0e54a8"}],"startTime":1717262705894681,"duration":38,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"StartState"},{"key":"emitter","type":"string","value":"bootstrapFlow"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ec61312ac083bcee","operationName":"BootstrapCheckingState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"6d3258ba9b0e54a8"}],"startTime":1717262705894734,"duration":48,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrapCheckingState"},{"key":"emitter","type":"string","value":"bootstrapFlow"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"6d3258ba9b0e54a8","operationName":"[add] Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"6ce192201667b3b1"}],"startTime":1717262705894612,"duration":187,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"688ca7899e223b4a41c4d0f2dec10130"},{"key":"time_before","type":"int64","value":0},{"key":"mutation","type":"string","value":"[add] Start"},{"key":"states_diff","type":"string","value":"+Start +BootstrapChecking"},{"key":"time_after","type":"int64","value":2},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":8},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"bcd1964b869015c2","operationName":"[add] TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b6f2aaf0f8d7809a"}],"startTime":1717262705895110,"duration":149,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"9b6cae1c5eac0c5187e664668dbf8c54"},{"key":"time_before","type":"int64","value":12},{"key":"mutation","type":"string","value":"[add] TopicBootstrapped"},{"key":"args.topic","type":"string","value":"foobar"},{"key":"states_diff","type":"string","value":"+TopicBootstrapped"},{"key":"time_after","type":"int64","value":14},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"e777f3bf3be6aebb","operationName":"BootstrapChecking","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"cc20bd3db35d545e"}],"startTime":1717262705894812,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"304d50e71fe030af","operationName":"[add] TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"6ce192201667b3b1"}],"startTime":1717262705894836,"duration":73,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"395a171b287c19de4d4ab62c31bfb4d9"},{"key":"time_before","type":"int64","value":2},{"key":"mutation","type":"string","value":"[add] TopicBootstrapped"},{"key":"states_diff","type":"string","value":"+TopicBootstrapped -BootstrapChecking"},{"key":"time_after","type":"int64","value":4},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":5},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ef8c86025d66432e","operationName":"BootstrapChecking","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"e777f3bf3be6aebb"}],"startTime":1717262705894816,"duration":95,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"688ca7899e223b4a41c4d0f2dec10130"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a7ade8a491a94607","operationName":"TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"cc20bd3db35d545e"}],"startTime":1717262705894915,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0b03d6b79fc3a2e4","operationName":"BootstrappingTopicState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"de41ca440e4295c8"}],"startTime":1717262705892823,"duration":2109,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrappingTopicState"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"de41ca440e4295c8","operationName":"[add] BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b6f2aaf0f8d7809a"}],"startTime":1717262705892695,"duration":2318,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"265965caa143886b1f9c9727b82d7420"},{"key":"time_before","type":"int64","value":11},{"key":"mutation","type":"string","value":"[add] BootstrappingTopic"},{"key":"args.topic","type":"string","value":"foobar"},{"key":"states_diff","type":"string","value":"+BootstrappingTopic"},{"key":"time_after","type":"int64","value":12},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"45caaaa248f6565a","operationName":"Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"34aabe3d17c2a9e3"}],"startTime":1717262705894810,"duration":239,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"688ca7899e223b4a41c4d0f2dec10130"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"729ac2d41f814735","operationName":"TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a7ade8a491a94607"}],"startTime":1717262705894919,"duration":159,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"395a171b287c19de4d4ab62c31bfb4d9"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"8f54b693e692cd83","operationName":"mach:ps-1-disc-bf-3-foobar","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"8a4e738ccae9bfee"}],"startTime":1717262705892906,"duration":2177,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"id","type":"string","value":"ps-1-disc-bf-3-foobar"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"621fa1b81a2c6314","operationName":"TopicBootstrappedState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"bcd1964b869015c2"}],"startTime":1717262705895192,"duration":33,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"TopicBootstrappedState"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7b6695151df34df1","operationName":"PublishMessageEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"29524bb861de12da"}],"startTime":1717262705896593,"duration":14,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d1d5e6fe69d76df0","operationName":"TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"3cd8aeeb719e3faa"}],"startTime":1717262705889422,"duration":5841,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"7209476f5487f9fb2699a6c206c44660"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"2807d50ef4fcb568","operationName":"BootstrappingTopicExit","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"2bed3a927b779961"}],"startTime":1717262705895354,"duration":23,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrappingTopicExit"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"2bed3a927b779961","operationName":"[remove] BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b6f2aaf0f8d7809a"}],"startTime":1717262705895298,"duration":137,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"cba983430cbed5accad30bd1e6623e77"},{"key":"time_before","type":"int64","value":14},{"key":"mutation","type":"string","value":"[remove] BootstrappingTopic"},{"key":"args.topic","type":"string","value":"foobar"},{"key":"states_diff","type":"string","value":"-BootstrappingTopic"},{"key":"time_after","type":"int64","value":15},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"80a2c06be9afe01f","operationName":"BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7377dc17849ae3f2"}],"startTime":1717262705895019,"duration":419,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"265965caa143886b1f9c9727b82d7420"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"e90cc828516908ee","operationName":"[remove] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705895977,"duration":65,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"4e57d6f8667ed1d2999cf89897b327e4"},{"key":"time_before","type":"int64","value":27},{"key":"mutation","type":"string","value":"[remove] PublishMessage"},{"key":"states_diff","type":"string","value":"-PublishMessage"},{"key":"time_after","type":"int64","value":28},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c0f5b60db244b0e9","operationName":"[add] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705895805,"duration":133,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"b2fd620d49cef13f76c1aed836a66835"},{"key":"time_before","type":"int64","value":26},{"key":"mutation","type":"string","value":"[add] PublishMessage"},{"key":"states_diff","type":"string","value":"+PublishMessage"},{"key":"time_after","type":"int64","value":27},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"75896042c0f19a73","operationName":"PublishMessageEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7e6d99c76062968a"}],"startTime":1717262705896552,"duration":24,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b50390aabea25f0b","operationName":"[remove] BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b6f2aaf0f8d7809a"}],"startTime":1717262705895462,"duration":45,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"a0394318c3746387a218d6c76f78bf6c"},{"key":"time_before","type":"int64","value":15},{"key":"mutation","type":"string","value":"[remove] BootstrappingTopic"},{"key":"states_diff","type":"string","value":""},{"key":"time_after","type":"int64","value":15},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"608565931ef95fc6","operationName":"PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"cd741da4d202b261"}],"startTime":1717262705895944,"duration":105,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"b2fd620d49cef13f76c1aed836a66835"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"cbcc6d203c3111ef","operationName":"BootstrappingTopicEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"8d0d91af0b0ce130"}],"startTime":1717262705896944,"duration":19,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrappingTopicEnter"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7e51c25651b97e63","operationName":"[add] Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b32cc038434ffddb"}],"startTime":1717262705898896,"duration":165,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"6e82af00aca3154fa113a56040c511d5"},{"key":"time_before","type":"int64","value":0},{"key":"mutation","type":"string","value":"[add] Start"},{"key":"states_diff","type":"string","value":"+Start +BootstrapChecking"},{"key":"time_after","type":"int64","value":2},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":8},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"2ec62eefc077fef2","operationName":"PublishMessageState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c0f5b60db244b0e9"}],"startTime":1717262705895885,"duration":19,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"2e3e2b650ddf098b","operationName":"PublishMessageState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7e6d99c76062968a"}],"startTime":1717262705896608,"duration":26,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b32cc038434ffddb","operationName":"transitions","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"dc7da0c3e6173c86"}],"startTime":1717262705897154,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-0-disc-bf-5-foobar"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"806593ee7c184e4d","operationName":"PublishMessageEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c0f5b60db244b0e9"}],"startTime":1717262705895846,"duration":20,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"10feceb9c5d8af97","operationName":"PublishMessageState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"29524bb861de12da"}],"startTime":1717262705896626,"duration":22,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ea1b11af8ead8854","operationName":"states","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"dc7da0c3e6173c86"}],"startTime":1717262705897139,"duration":3,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-0-disc-bf-5-foobar"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0571afa67fc707d2","operationName":"StartState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7e51c25651b97e63"}],"startTime":1717262705898970,"duration":28,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"StartState"},{"key":"emitter","type":"string","value":"bootstrapFlow"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"479d85337f563f49","operationName":"Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ea1b11af8ead8854"}],"startTime":1717262705899065,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"77729143c36dbe44","operationName":"[remove] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705896724,"duration":52,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"7f4e8ca1e9cc0c3fe364973184d00044"},{"key":"time_before","type":"int64","value":27},{"key":"mutation","type":"string","value":"[remove] PublishMessage"},{"key":"states_diff","type":"string","value":"-PublishMessage"},{"key":"time_after","type":"int64","value":28},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"74b87c54b7bf65fe","operationName":"BootstrapCheckingState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7e51c25651b97e63"}],"startTime":1717262705899009,"duration":39,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrapCheckingState"},{"key":"emitter","type":"string","value":"bootstrapFlow"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"52d95151e8696123","operationName":"BootstrapChecking","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ea1b11af8ead8854"}],"startTime":1717262705899070,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"e4893339038f72f8","operationName":"PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"fcf8d9dbe6eea793"}],"startTime":1717262705896694,"duration":86,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"952b73123032122e45ba0596438d9c1d"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"4d09a7a024d73fb7","operationName":"[add] TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b32cc038434ffddb"}],"startTime":1717262705899087,"duration":48,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"68fa4231d77488c202d24de372ee866e"},{"key":"time_before","type":"int64","value":2},{"key":"mutation","type":"string","value":"[add] TopicBootstrapped"},{"key":"states_diff","type":"string","value":"+TopicBootstrapped -BootstrapChecking"},{"key":"time_after","type":"int64","value":4},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":5},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7e6d99c76062968a","operationName":"[add] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705896513,"duration":161,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"74eae09946656fc336f814d32eae5a6b"},{"key":"time_before","type":"int64","value":26},{"key":"mutation","type":"string","value":"[add] PublishMessage"},{"key":"states_diff","type":"string","value":"+PublishMessage"},{"key":"time_after","type":"int64","value":27},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"fbe9854302bd7405","operationName":"[remove] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705896712,"duration":70,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"060967db6a3029fa526566868f0680dd"},{"key":"time_before","type":"int64","value":27},{"key":"mutation","type":"string","value":"[remove] PublishMessage"},{"key":"states_diff","type":"string","value":"-PublishMessage"},{"key":"time_after","type":"int64","value":28},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"29524bb861de12da","operationName":"[add] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705896558,"duration":129,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"952b73123032122e45ba0596438d9c1d"},{"key":"time_before","type":"int64","value":26},{"key":"mutation","type":"string","value":"[add] PublishMessage"},{"key":"states_diff","type":"string","value":"+PublishMessage"},{"key":"time_after","type":"int64","value":27},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"45fb5d7e5dbcbfca","operationName":"BootstrapChecking","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"52d95151e8696123"}],"startTime":1717262705899073,"duration":64,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"6e82af00aca3154fa113a56040c511d5"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c67d7890b2764b67","operationName":"PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"031e417fb7dda017"}],"startTime":1717262705896682,"duration":104,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"74eae09946656fc336f814d32eae5a6b"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"af10586e09b37931","operationName":"PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"fcf8d9dbe6eea793"}],"startTime":1717262705900048,"duration":94,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"99d61e9e7ec00b8cceea00e9a3a34c7b"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d191fda938173e5f","operationName":"TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ea1b11af8ead8854"}],"startTime":1717262705899139,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"5e9377189e84906c","operationName":"BootstrappingTopicState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"8d0d91af0b0ce130"}],"startTime":1717262705896993,"duration":2160,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrappingTopicState"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"8d0d91af0b0ce130","operationName":"[add] BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a47565b8219f873b"}],"startTime":1717262705896869,"duration":2304,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"a9679175c5d0b2efd63bc2c97e088261"},{"key":"time_before","type":"int64","value":19},{"key":"mutation","type":"string","value":"[add] BootstrappingTopic"},{"key":"args.topic","type":"string","value":"foobar"},{"key":"states_diff","type":"string","value":"+BootstrappingTopic"},{"key":"time_after","type":"int64","value":20},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"09a717e8a5407770","operationName":"TopicBootstrappedState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1d3148416d81cee9"}],"startTime":1717262705899421,"duration":38,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"TopicBootstrappedState"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"bdccaf3031ef4885","operationName":"BootstrapCheckingState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"69d758358d710cc0"}],"startTime":1717262705903420,"duration":48,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrapCheckingState"},{"key":"emitter","type":"string","value":"bootstrapFlow"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"aa7a786b90a1e4aa","operationName":"BootstrappingTopicExit","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"3fe036209afd82af"}],"startTime":1717262705899588,"duration":20,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrappingTopicExit"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"3fe036209afd82af","operationName":"[remove] BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a47565b8219f873b"}],"startTime":1717262705899542,"duration":145,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"bda5c1a53d1c14d8c6feee85c593d81f"},{"key":"time_before","type":"int64","value":22},{"key":"mutation","type":"string","value":"[remove] BootstrappingTopic"},{"key":"states_diff","type":"string","value":"-BootstrappingTopic"},{"key":"time_after","type":"int64","value":23},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1d3148416d81cee9","operationName":"[add] TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a47565b8219f873b"}],"startTime":1717262705899315,"duration":189,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"83b5dc53fe112cfa81797c13c64ce2a9"},{"key":"time_before","type":"int64","value":20},{"key":"mutation","type":"string","value":"[add] TopicBootstrapped"},{"key":"args.topic","type":"string","value":"foobar"},{"key":"states_diff","type":"string","value":"+TopicBootstrapped"},{"key":"time_after","type":"int64","value":22},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f02adb86e7172dc6","operationName":"Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"479d85337f563f49"}],"startTime":1717262705899069,"duration":683,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"6e82af00aca3154fa113a56040c511d5"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"44333d795b818d9a","operationName":"TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d12d683e8445bad4"}],"startTime":1717262705880276,"duration":19233,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"1423d3868e675089622d9729d543ac13"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"89a418932f267677","operationName":"TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d191fda938173e5f"}],"startTime":1717262705899143,"duration":610,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"68fa4231d77488c202d24de372ee866e"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9d39730c9c7c7e28","operationName":"BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"16f8c5c868c37c27"}],"startTime":1717262705899177,"duration":514,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"a9679175c5d0b2efd63bc2c97e088261"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"dc7da0c3e6173c86","operationName":"mach:ps-0-disc-bf-5-foobar","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"881d141acaa59733"}],"startTime":1717262705897096,"duration":2660,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"id","type":"string","value":"ps-0-disc-bf-5-foobar"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"6f6c090116e00c5c","operationName":"PublishMessageEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d9b4440a902bf07d"}],"startTime":1717262705900755,"duration":27,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7ceeccce8cbbbe40","operationName":"PublishMessageEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"5d7febd502cc3d1f"}],"startTime":1717262705899927,"duration":25,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d2792daeb348b46f","operationName":"PublishMessageState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"5d7febd502cc3d1f"}],"startTime":1717262705899975,"duration":30,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d397439fbe45ef7d","operationName":"PublishMessageEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b4f9f8a900ad39f8"}],"startTime":1717262705900810,"duration":17,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"5d7febd502cc3d1f","operationName":"[add] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705899891,"duration":150,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"99d61e9e7ec00b8cceea00e9a3a34c7b"},{"key":"time_before","type":"int64","value":28},{"key":"mutation","type":"string","value":"[add] PublishMessage"},{"key":"states_diff","type":"string","value":"+PublishMessage"},{"key":"time_after","type":"int64","value":29},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c688ba2c4612e85e","operationName":"PublishMessageState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d9b4440a902bf07d"}],"startTime":1717262705900809,"duration":33,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"342274f3f60b5b3f","operationName":"[remove] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705900080,"duration":59,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"34f290ca0c214a194a963135adc6ffab"},{"key":"time_before","type":"int64","value":29},{"key":"mutation","type":"string","value":"[remove] PublishMessage"},{"key":"states_diff","type":"string","value":"-PublishMessage"},{"key":"time_after","type":"int64","value":30},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"69d758358d710cc0","operationName":"[add] Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"26e0867d69564299"}],"startTime":1717262705903292,"duration":193,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"ea6ddf01bf45e93f82698cfaa5c33a4b"},{"key":"time_before","type":"int64","value":0},{"key":"mutation","type":"string","value":"[add] Start"},{"key":"states_diff","type":"string","value":"+Start +BootstrapChecking"},{"key":"time_after","type":"int64","value":2},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":8},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d9b4440a902bf07d","operationName":"[add] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705900704,"duration":184,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"09cf5e2c75a29164bf12269f27f9aae5"},{"key":"time_before","type":"int64","value":28},{"key":"mutation","type":"string","value":"[add] PublishMessage"},{"key":"states_diff","type":"string","value":"+PublishMessage"},{"key":"time_after","type":"int64","value":29},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"cf39870c114aa3ec","operationName":"Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b0b0b535fa820d20"}],"startTime":1717262705903492,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"65503092c8b5a6c9","operationName":"PublishMessageState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b4f9f8a900ad39f8"}],"startTime":1717262705900851,"duration":63,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"86090c5c143f13c7","operationName":"BootstrapChecking","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b0b0b535fa820d20"}],"startTime":1717262705903500,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b4f9f8a900ad39f8","operationName":"[add] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705900760,"duration":197,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"4e59b65097588e7930e230ec6974ed5f"},{"key":"time_before","type":"int64","value":28},{"key":"mutation","type":"string","value":"[add] PublishMessage"},{"key":"states_diff","type":"string","value":"+PublishMessage"},{"key":"time_after","type":"int64","value":29},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7f1b69b388eacda6","operationName":"[add] TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"26e0867d69564299"}],"startTime":1717262705903524,"duration":79,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"74149bfbadec19c27a0d685e51c02247"},{"key":"time_before","type":"int64","value":2},{"key":"mutation","type":"string","value":"[add] TopicBootstrapped"},{"key":"states_diff","type":"string","value":"+TopicBootstrapped -BootstrapChecking"},{"key":"time_after","type":"int64","value":4},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":5},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"54f43a3414c88a2e","operationName":"BootstrappingTopicEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d6624af715f2b6ea"}],"startTime":1717262705901257,"duration":28,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrappingTopicEnter"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"304a62fbc2cd12ee","operationName":"BootstrapChecking","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"86090c5c143f13c7"}],"startTime":1717262705903504,"duration":103,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"ea6ddf01bf45e93f82698cfaa5c33a4b"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b0b0b535fa820d20","operationName":"states","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"43589245e699f4f8"}],"startTime":1717262705901408,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-1-disc-bf-5-foobar"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"02f1def52659635e","operationName":"TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b0b0b535fa820d20"}],"startTime":1717262705903612,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"21dd51ff6cf65900","operationName":"PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"cd741da4d202b261"}],"startTime":1717262705900965,"duration":112,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"4e59b65097588e7930e230ec6974ed5f"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"26e0867d69564299","operationName":"transitions","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"43589245e699f4f8"}],"startTime":1717262705901419,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-1-disc-bf-5-foobar"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"08a25db197b905b3","operationName":"TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"02f1def52659635e"}],"startTime":1717262705903616,"duration":115,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"74149bfbadec19c27a0d685e51c02247"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"e3d4f0c7bd7c139e","operationName":"StartState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"69d758358d710cc0"}],"startTime":1717262705903370,"duration":35,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"StartState"},{"key":"emitter","type":"string","value":"bootstrapFlow"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"fc2c121354b4d76e","operationName":"TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"3cd8aeeb719e3faa"}],"startTime":1717262705895268,"duration":8655,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"9b6cae1c5eac0c5187e664668dbf8c54"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7039627182adcc81","operationName":"BootstrappingTopicExit","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"344313b207adc303"}],"startTime":1717262705903999,"duration":13,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrappingTopicExit"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"43589245e699f4f8","operationName":"mach:ps-1-disc-bf-5-foobar","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"8a4e738ccae9bfee"}],"startTime":1717262705901367,"duration":2398,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"id","type":"string","value":"ps-1-disc-bf-5-foobar"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"315b5dc6271fe5b8","operationName":"TopicBootstrappedState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f3f2322f45cc68fc"}],"startTime":1717262705903862,"duration":26,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"TopicBootstrappedState"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"00eacfccfa09ee12","operationName":"[remove] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705900929,"duration":65,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"4fd8a000f1b148c8467e8ab15c33de3b"},{"key":"time_before","type":"int64","value":29},{"key":"mutation","type":"string","value":"[remove] PublishMessage"},{"key":"states_diff","type":"string","value":"-PublishMessage"},{"key":"time_after","type":"int64","value":30},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0edf4cedbedb6237","operationName":"Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"cf39870c114aa3ec"}],"startTime":1717262705903497,"duration":263,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"ea6ddf01bf45e93f82698cfaa5c33a4b"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1375ff233a0d8930","operationName":"[remove] BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b6f2aaf0f8d7809a"}],"startTime":1717262705904114,"duration":44,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"1970f6314d5ceda8a4c29c54b9abd908"},{"key":"time_before","type":"int64","value":19},{"key":"mutation","type":"string","value":"[remove] BootstrappingTopic"},{"key":"states_diff","type":"string","value":""},{"key":"time_after","type":"int64","value":19},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f3f2322f45cc68fc","operationName":"[add] TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b6f2aaf0f8d7809a"}],"startTime":1717262705903786,"duration":133,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"a780a5d7ed7ef864eee3bf05d403ac35"},{"key":"time_before","type":"int64","value":16},{"key":"mutation","type":"string","value":"[add] TopicBootstrapped"},{"key":"args.topic","type":"string","value":"foobar"},{"key":"states_diff","type":"string","value":"+TopicBootstrapped"},{"key":"time_after","type":"int64","value":18},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ff930074e2be949e","operationName":"BootstrappingTopicState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d6624af715f2b6ea"}],"startTime":1717262705901314,"duration":2327,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrappingTopicState"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0d7ceaa73363e738","operationName":"PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"031e417fb7dda017"}],"startTime":1717262705900897,"duration":102,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"09cf5e2c75a29164bf12269f27f9aae5"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b3cee09c6766f9d5","operationName":"[remove] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705900999,"duration":73,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"f916ea97d9a0a49d5e67317ec115517d"},{"key":"time_before","type":"int64","value":29},{"key":"mutation","type":"string","value":"[remove] PublishMessage"},{"key":"states_diff","type":"string","value":"-PublishMessage"},{"key":"time_after","type":"int64","value":30},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"344313b207adc303","operationName":"[remove] BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b6f2aaf0f8d7809a"}],"startTime":1717262705903952,"duration":129,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"46b10a22369cde681f8f696a893ecab6"},{"key":"time_before","type":"int64","value":18},{"key":"mutation","type":"string","value":"[remove] BootstrappingTopic"},{"key":"args.topic","type":"string","value":"foobar"},{"key":"states_diff","type":"string","value":"-BootstrappingTopic"},{"key":"time_after","type":"int64","value":19},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d6624af715f2b6ea","operationName":"[add] BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b6f2aaf0f8d7809a"}],"startTime":1717262705901192,"duration":2484,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"7bfb684cf910ca9fbc9bb10075a4d48d"},{"key":"time_before","type":"int64","value":15},{"key":"mutation","type":"string","value":"[add] BootstrappingTopic"},{"key":"args.topic","type":"string","value":"foobar"},{"key":"states_diff","type":"string","value":"+BootstrappingTopic"},{"key":"time_after","type":"int64","value":16},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1cb034c48064248d","operationName":"BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7377dc17849ae3f2"}],"startTime":1717262705903683,"duration":406,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"7bfb684cf910ca9fbc9bb10075a4d48d"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"50bc7972d0b21a0a","operationName":"PublishMessageState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"17ced506e97a47ec"}],"startTime":1717262705904532,"duration":24,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d65513269ab384a3","operationName":"PublishMessageEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"5a9c203fb60745d4"}],"startTime":1717262705905125,"duration":25,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"cb35adf8de143e7f","operationName":"PublishMessageEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"17ced506e97a47ec"}],"startTime":1717262705904484,"duration":24,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0830d7d614e61632","operationName":"PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"cd741da4d202b261"}],"startTime":1717262705904595,"duration":91,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"a985fda428dc6c4b9abfa4f6f764457c"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"17ced506e97a47ec","operationName":"[add] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705904441,"duration":148,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"a985fda428dc6c4b9abfa4f6f764457c"},{"key":"time_before","type":"int64","value":30},{"key":"mutation","type":"string","value":"[add] PublishMessage"},{"key":"states_diff","type":"string","value":"+PublishMessage"},{"key":"time_after","type":"int64","value":31},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"451ee4dc60104833","operationName":"[remove] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705904629,"duration":55,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"0101bc59eaef7010db8567e4e021d304"},{"key":"time_before","type":"int64","value":31},{"key":"mutation","type":"string","value":"[remove] PublishMessage"},{"key":"states_diff","type":"string","value":"-PublishMessage"},{"key":"time_after","type":"int64","value":32},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"5a9c203fb60745d4","operationName":"[add] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705905095,"duration":178,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"88a13a7ce88ce3aa2abaf383f8788197"},{"key":"time_before","type":"int64","value":30},{"key":"mutation","type":"string","value":"[add] PublishMessage"},{"key":"states_diff","type":"string","value":"+PublishMessage"},{"key":"time_after","type":"int64","value":31},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"218d824230ba6c0c","operationName":"[remove] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705905318,"duration":117,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"936fc7132e9be2e0391ada0203c13f7b"},{"key":"time_before","type":"int64","value":31},{"key":"mutation","type":"string","value":"[remove] PublishMessage"},{"key":"states_diff","type":"string","value":"-PublishMessage"},{"key":"time_after","type":"int64","value":32},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"4f5c364a2d841104","operationName":"[add] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705905236,"duration":174,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"e10f245cf3b8084267764a37450a25f5"},{"key":"time_before","type":"int64","value":30},{"key":"mutation","type":"string","value":"[add] PublishMessage"},{"key":"states_diff","type":"string","value":"+PublishMessage"},{"key":"time_after","type":"int64","value":31},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d8ecda67628eace6","operationName":"PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"031e417fb7dda017"}],"startTime":1717262705905283,"duration":156,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"88a13a7ce88ce3aa2abaf383f8788197"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"93eebf8948ac99f3","operationName":"PublishMessageState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"4f5c364a2d841104"}],"startTime":1717262705905330,"duration":32,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"e8ab6b8b3b0f43da","operationName":"PublishMessageState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"5a9c203fb60745d4"}],"startTime":1717262705905180,"duration":50,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"aa38fe8857edafe6","operationName":"PublishMessageEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"4f5c364a2d841104"}],"startTime":1717262705905281,"duration":22,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"3ebda385c75703ef","operationName":"states","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f9fb085c275560c2"}],"startTime":1717262705905962,"duration":2,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-0-disc-bf-7-foobar"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"792e4c55828ce5f8","operationName":"TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"3ebda385c75703ef"}],"startTime":1717262705907989,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"dfdecd084e0bdf6e","operationName":"[add] Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"276843f0ba6dd55c"}],"startTime":1717262705907716,"duration":181,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"6b1dfecc7ce06aa9719051cc344f2b48"},{"key":"time_before","type":"int64","value":0},{"key":"mutation","type":"string","value":"[add] Start"},{"key":"states_diff","type":"string","value":"+Start +BootstrapChecking"},{"key":"time_after","type":"int64","value":2},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":8},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c7433bf6aa0e5bf0","operationName":"PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"fcf8d9dbe6eea793"}],"startTime":1717262705905418,"duration":110,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"e10f245cf3b8084267764a37450a25f5"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1b1188e8b31aa295","operationName":"[remove] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705905460,"duration":64,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"cb7f216239fab1011601d73fd2e1caa4"},{"key":"time_before","type":"int64","value":31},{"key":"mutation","type":"string","value":"[remove] PublishMessage"},{"key":"states_diff","type":"string","value":"-PublishMessage"},{"key":"time_after","type":"int64","value":32},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"276843f0ba6dd55c","operationName":"transitions","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f9fb085c275560c2"}],"startTime":1717262705905971,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-0-disc-bf-7-foobar"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"774b3582e415bb7f","operationName":"StartState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"dfdecd084e0bdf6e"}],"startTime":1717262705907770,"duration":31,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"StartState"},{"key":"emitter","type":"string","value":"bootstrapFlow"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c2cb7c565e6f03c4","operationName":"BootstrapChecking","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"3ebda385c75703ef"}],"startTime":1717262705907907,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7993cc9f48233f19","operationName":"BootstrappingTopicEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"761a0634decfc66b"}],"startTime":1717262705905690,"duration":20,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrappingTopicEnter"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"da2072518def6a72","operationName":"BootstrapCheckingState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"dfdecd084e0bdf6e"}],"startTime":1717262705907829,"duration":53,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrapCheckingState"},{"key":"emitter","type":"string","value":"bootstrapFlow"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"8263e9419ee0b3dc","operationName":"BootstrappingTopicState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"761a0634decfc66b"}],"startTime":1717262705905740,"duration":2263,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrappingTopicState"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"34272e6227b0258b","operationName":"Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"3ebda385c75703ef"}],"startTime":1717262705907901,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"761a0634decfc66b","operationName":"[add] BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a47565b8219f873b"}],"startTime":1717262705905619,"duration":2406,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"6ed308a3d5ed9c9b2480b004c0ecdaa0"},{"key":"time_before","type":"int64","value":23},{"key":"mutation","type":"string","value":"[add] BootstrappingTopic"},{"key":"args.topic","type":"string","value":"foobar"},{"key":"states_diff","type":"string","value":"+BootstrappingTopic"},{"key":"time_after","type":"int64","value":24},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d1138baa5feb665a","operationName":"TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d12d683e8445bad4"}],"startTime":1717262705899515,"duration":8704,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"83b5dc53fe112cfa81797c13c64ce2a9"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"459bf0e61077c6e0","operationName":"TopicBootstrappedState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"8ca629561dc95fd9"}],"startTime":1717262705908160,"duration":24,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"TopicBootstrappedState"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"8ca629561dc95fd9","operationName":"[add] TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a47565b8219f873b"}],"startTime":1717262705908093,"duration":122,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"58a1c5db0481dc78d9e6fa9b0fa5bb63"},{"key":"time_before","type":"int64","value":24},{"key":"mutation","type":"string","value":"[add] TopicBootstrapped"},{"key":"args.topic","type":"string","value":"foobar"},{"key":"states_diff","type":"string","value":"+TopicBootstrapped"},{"key":"time_after","type":"int64","value":26},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"eb928b6a21373be6","operationName":"Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"34272e6227b0258b"}],"startTime":1717262705907904,"duration":494,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"6b1dfecc7ce06aa9719051cc344f2b48"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"cab69afea447d16f","operationName":"[remove] BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a47565b8219f873b"}],"startTime":1717262705908241,"duration":108,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"f048c3d4588bc44eaf2293e35299b858"},{"key":"time_before","type":"int64","value":26},{"key":"mutation","type":"string","value":"[remove] BootstrappingTopic"},{"key":"states_diff","type":"string","value":"-BootstrappingTopic"},{"key":"time_after","type":"int64","value":27},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f9fb085c275560c2","operationName":"mach:ps-0-disc-bf-7-foobar","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"881d141acaa59733"}],"startTime":1717262705905801,"duration":2602,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"id","type":"string","value":"ps-0-disc-bf-7-foobar"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"5a1218b4b060ff35","operationName":"BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"16f8c5c868c37c27"}],"startTime":1717262705908032,"duration":320,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"6ed308a3d5ed9c9b2480b004c0ecdaa0"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9e214d7b5ca541bf","operationName":"TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"792e4c55828ce5f8"}],"startTime":1717262705907992,"duration":409,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"a56886f9069ce4d3e7748ba110cd5d26"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d240703d5496f452","operationName":"PublishMessageState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"511c68c636004f1a"}],"startTime":1717262705908679,"duration":17,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"e3c11d82616ff305","operationName":"PublishMessageEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"511c68c636004f1a"}],"startTime":1717262705908639,"duration":24,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"8ec7799358c8e8c1","operationName":"[remove] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705908755,"duration":47,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"0c9807d116493cf2e89465623e5c23bb"},{"key":"time_before","type":"int64","value":33},{"key":"mutation","type":"string","value":"[remove] PublishMessage"},{"key":"states_diff","type":"string","value":"-PublishMessage"},{"key":"time_after","type":"int64","value":34},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"726d10a32b2aa4d0","operationName":"PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"fcf8d9dbe6eea793"}],"startTime":1717262705908731,"duration":74,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"22d938468ac5e486cdacb0838802cfe7"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"511c68c636004f1a","operationName":"[add] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705908610,"duration":116,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"22d938468ac5e486cdacb0838802cfe7"},{"key":"time_before","type":"int64","value":32},{"key":"mutation","type":"string","value":"[add] PublishMessage"},{"key":"states_diff","type":"string","value":"+PublishMessage"},{"key":"time_after","type":"int64","value":33},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"69188736c889df7d","operationName":"[add] TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"276843f0ba6dd55c"}],"startTime":1717262705907925,"duration":57,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"a56886f9069ce4d3e7748ba110cd5d26"},{"key":"time_before","type":"int64","value":2},{"key":"mutation","type":"string","value":"[add] TopicBootstrapped"},{"key":"states_diff","type":"string","value":"+TopicBootstrapped -BootstrapChecking"},{"key":"time_after","type":"int64","value":4},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":5},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ab019b789027b75e","operationName":"PublishMessageEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"da77e5928ca23c1b"}],"startTime":1717262705909107,"duration":15,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"3ac3d247bd8a9b0f","operationName":"BootstrappingTopicExit","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"cab69afea447d16f"}],"startTime":1717262705908284,"duration":13,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrappingTopicExit"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"fa504ae70f9a0140","operationName":"BootstrapChecking","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c2cb7c565e6f03c4"}],"startTime":1717262705907911,"duration":74,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"6b1dfecc7ce06aa9719051cc344f2b48"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9d92b65b676fe729","operationName":"PublishMessageEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"3feb168b0f5fc6db"}],"startTime":1717262705909219,"duration":21,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1e67f7e946737ed9","operationName":"PublishMessageState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"da77e5928ca23c1b"}],"startTime":1717262705909135,"duration":14,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"da77e5928ca23c1b","operationName":"[add] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705909084,"duration":87,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"88f917e038d3f9ec413aac1d622f2f5a"},{"key":"time_before","type":"int64","value":32},{"key":"mutation","type":"string","value":"[add] PublishMessage"},{"key":"states_diff","type":"string","value":"+PublishMessage"},{"key":"time_after","type":"int64","value":33},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"39d4876b0075e055","operationName":"PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"cd741da4d202b261"}],"startTime":1717262705909176,"duration":72,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"88f917e038d3f9ec413aac1d622f2f5a"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"30334dfe84fd04ed","operationName":"[remove] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705909194,"duration":51,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"742aa378b1a2aa15d2a15509ff893f35"},{"key":"time_before","type":"int64","value":33},{"key":"mutation","type":"string","value":"[remove] PublishMessage"},{"key":"states_diff","type":"string","value":"-PublishMessage"},{"key":"time_after","type":"int64","value":34},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c5ddcbd7d2dc7c6f","operationName":"PublishMessageState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"3feb168b0f5fc6db"}],"startTime":1717262705909257,"duration":21,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"3feb168b0f5fc6db","operationName":"[add] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705909187,"duration":158,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"ed20cc4dfe7a5b74e2811e716ff202e4"},{"key":"time_before","type":"int64","value":32},{"key":"mutation","type":"string","value":"[add] PublishMessage"},{"key":"states_diff","type":"string","value":"+PublishMessage"},{"key":"time_after","type":"int64","value":33},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ea7caec7924bbb24","operationName":"[remove] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705909382,"duration":46,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"be4d23a774b3db755650be1975c08688"},{"key":"time_before","type":"int64","value":33},{"key":"mutation","type":"string","value":"[remove] PublishMessage"},{"key":"states_diff","type":"string","value":"-PublishMessage"},{"key":"time_after","type":"int64","value":34},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"638859cf7075cee7","operationName":"PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"031e417fb7dda017"}],"startTime":1717262705909352,"duration":79,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"ed20cc4dfe7a5b74e2811e716ff202e4"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b6b9d068d0ef81c6","operationName":"BootstrapCheckingState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"455d47dfeab89ca5"}],"startTime":1717262705911343,"duration":42,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrapCheckingState"},{"key":"emitter","type":"string","value":"bootstrapFlow"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"cc719991f0cc3214","operationName":"BootstrapChecking","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"39b2e7cb9f8cb3a0"}],"startTime":1717262705911413,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ac1bbc4cc0e1e6e8","operationName":"BootstrappingTopicEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b11a55c9ef2d4225"}],"startTime":1717262705909504,"duration":20,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrappingTopicEnter"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"39b2e7cb9f8cb3a0","operationName":"states","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a006e6df261b83a4"}],"startTime":1717262705909629,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-1-disc-bf-7-foobar"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"455d47dfeab89ca5","operationName":"[add] Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"387cce9464790fb4"}],"startTime":1717262705911245,"duration":157,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"f6dea51b3ceebc03f2fe6523a6d6eae1"},{"key":"time_before","type":"int64","value":0},{"key":"mutation","type":"string","value":"[add] Start"},{"key":"states_diff","type":"string","value":"+Start +BootstrapChecking"},{"key":"time_after","type":"int64","value":2},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":8},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"387cce9464790fb4","operationName":"transitions","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a006e6df261b83a4"}],"startTime":1717262705909635,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-1-disc-bf-7-foobar"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"05fa20bb8ea2e9bc","operationName":"Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"39b2e7cb9f8cb3a0"}],"startTime":1717262705911407,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"fa5b1878a8d26124","operationName":"StartState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"455d47dfeab89ca5"}],"startTime":1717262705911298,"duration":32,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"StartState"},{"key":"emitter","type":"string","value":"bootstrapFlow"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9f117177d7548474","operationName":"[add] TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b6f2aaf0f8d7809a"}],"startTime":1717262705911633,"duration":119,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"eb81c9316886350e7e49b412fa648741"},{"key":"time_before","type":"int64","value":20},{"key":"mutation","type":"string","value":"[add] TopicBootstrapped"},{"key":"args.topic","type":"string","value":"foobar"},{"key":"states_diff","type":"string","value":"+TopicBootstrapped"},{"key":"time_after","type":"int64","value":22},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"582a553f0b38fa37","operationName":"[add] TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"387cce9464790fb4"}],"startTime":1717262705911435,"duration":57,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"231580ab0620faacc6665d3a03b8cead"},{"key":"time_before","type":"int64","value":2},{"key":"mutation","type":"string","value":"[add] TopicBootstrapped"},{"key":"states_diff","type":"string","value":"+TopicBootstrapped -BootstrapChecking"},{"key":"time_after","type":"int64","value":4},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":5},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d4ae3221181436b6","operationName":"TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"39b2e7cb9f8cb3a0"}],"startTime":1717262705911502,"duration":2,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"db3ae520347fa3c5","operationName":"BootstrapChecking","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"cc719991f0cc3214"}],"startTime":1717262705911418,"duration":81,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"f6dea51b3ceebc03f2fe6523a6d6eae1"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"90ebd8d3b72c01bd","operationName":"BootstrappingTopicState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b11a55c9ef2d4225"}],"startTime":1717262705909551,"duration":1969,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrappingTopicState"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1d6bd25bb7215df2","operationName":"TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d4ae3221181436b6"}],"startTime":1717262705911508,"duration":415,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"231580ab0620faacc6665d3a03b8cead"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b11a55c9ef2d4225","operationName":"[add] BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b6f2aaf0f8d7809a"}],"startTime":1717262705909433,"duration":2107,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"29923019f363090cc85d1b3486ad2ff0"},{"key":"time_before","type":"int64","value":19},{"key":"mutation","type":"string","value":"[add] BootstrappingTopic"},{"key":"args.topic","type":"string","value":"foobar"},{"key":"states_diff","type":"string","value":"+BootstrappingTopic"},{"key":"time_after","type":"int64","value":20},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b66f11304bf13b42","operationName":"TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"3cd8aeeb719e3faa"}],"startTime":1717262705903928,"duration":7828,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"a780a5d7ed7ef864eee3bf05d403ac35"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9595b42ac5572c37","operationName":"BootstrappingTopicExit","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"5e4a85763b741e30"}],"startTime":1717262705911814,"duration":11,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrappingTopicExit"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1c9a5cc719bf9de3","operationName":"TopicBootstrappedState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9f117177d7548474"}],"startTime":1717262705911700,"duration":24,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"TopicBootstrappedState"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"5e4a85763b741e30","operationName":"[remove] BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b6f2aaf0f8d7809a"}],"startTime":1717262705911778,"duration":97,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"c05e2d768de98170b2a430e35c503f60"},{"key":"time_before","type":"int64","value":22},{"key":"mutation","type":"string","value":"[remove] BootstrappingTopic"},{"key":"states_diff","type":"string","value":"-BootstrappingTopic"},{"key":"time_after","type":"int64","value":23},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d1293d79af941722","operationName":"BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"7377dc17849ae3f2"}],"startTime":1717262705911545,"duration":333,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"29923019f363090cc85d1b3486ad2ff0"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"5bc8112343eb2529","operationName":"Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"05fa20bb8ea2e9bc"}],"startTime":1717262705911411,"duration":510,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"f6dea51b3ceebc03f2fe6523a6d6eae1"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"843289b63db93781","operationName":"PublishMessageState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"36e76fb24f6a5e4e"}],"startTime":1717262705912652,"duration":16,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a006e6df261b83a4","operationName":"mach:ps-1-disc-bf-7-foobar","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"8a4e738ccae9bfee"}],"startTime":1717262705909597,"duration":2328,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"id","type":"string","value":"ps-1-disc-bf-7-foobar"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"8f35f401e9b109cb","operationName":"PublishMessageEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"14336186834555da"}],"startTime":1717262705912077,"duration":24,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f6c219b561f5d581","operationName":"PublishMessageState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"14336186834555da"}],"startTime":1717262705912120,"duration":18,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"14336186834555da","operationName":"[add] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705912049,"duration":123,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"d2ade25d1175a96c9ecdc422ff5a2315"},{"key":"time_before","type":"int64","value":34},{"key":"mutation","type":"string","value":"[add] PublishMessage"},{"key":"states_diff","type":"string","value":"+PublishMessage"},{"key":"time_after","type":"int64","value":35},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"524b599a143a3d52","operationName":"[remove] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705912200,"duration":45,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"0ff2e1b70ca91ea05d165de05b4b4c13"},{"key":"time_before","type":"int64","value":35},{"key":"mutation","type":"string","value":"[remove] PublishMessage"},{"key":"states_diff","type":"string","value":"-PublishMessage"},{"key":"time_after","type":"int64","value":36},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"3621341955d4f41e","operationName":"PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"cd741da4d202b261"}],"startTime":1717262705912177,"duration":87,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"d2ade25d1175a96c9ecdc422ff5a2315"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d0cd43d30b208263","operationName":"PublishMessageEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"36e76fb24f6a5e4e"}],"startTime":1717262705912626,"duration":10,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"682bbae7f00ab5ca","operationName":"PublishMessageEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0fbc45839bafa148"}],"startTime":1717262705912644,"duration":13,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"8895fe4c20a914b9","operationName":"states","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f8cebeca9787c875"}],"startTime":1717262705913051,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-2-disc-bf-3-foobar"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"382b97042b3ba137","operationName":"[add] Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0ab31c0d4ea9a782"}],"startTime":1717262705916649,"duration":152,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"97b4f7cbdd458fa906e66301bc635559"},{"key":"time_before","type":"int64","value":0},{"key":"mutation","type":"string","value":"[add] Start"},{"key":"states_diff","type":"string","value":"+Start +BootstrapChecking"},{"key":"time_after","type":"int64","value":2},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":8},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"36e76fb24f6a5e4e","operationName":"[add] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705912601,"duration":91,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"2081f8f1bb928e73430a50150058ec83"},{"key":"time_before","type":"int64","value":34},{"key":"mutation","type":"string","value":"[add] PublishMessage"},{"key":"states_diff","type":"string","value":"+PublishMessage"},{"key":"time_after","type":"int64","value":35},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0ab31c0d4ea9a782","operationName":"transitions","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f8cebeca9787c875"}],"startTime":1717262705913056,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-2-disc-bf-3-foobar"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"88cbed58cd507601","operationName":"StartState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"382b97042b3ba137"}],"startTime":1717262705916702,"duration":31,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"StartState"},{"key":"emitter","type":"string","value":"bootstrapFlow"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9009a146904d6f72","operationName":"PublishMessageState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0fbc45839bafa148"}],"startTime":1717262705912675,"duration":22,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"4c42108d65491f62","operationName":"BootstrapCheckingState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"382b97042b3ba137"}],"startTime":1717262705916752,"duration":37,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrapCheckingState"},{"key":"emitter","type":"string","value":"bootstrapFlow"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0fbc45839bafa148","operationName":"[add] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705912611,"duration":113,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"6d823cbbe1f8de73fc54b8b84db495e2"},{"key":"time_before","type":"int64","value":34},{"key":"mutation","type":"string","value":"[add] PublishMessage"},{"key":"states_diff","type":"string","value":"+PublishMessage"},{"key":"time_after","type":"int64","value":35},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"af81d3336fd0bf04","operationName":"[remove] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705912714,"duration":35,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"01f510fd038478f49fd312a9ce2fc9a2"},{"key":"time_before","type":"int64","value":35},{"key":"mutation","type":"string","value":"[remove] PublishMessage"},{"key":"states_diff","type":"string","value":"-PublishMessage"},{"key":"time_after","type":"int64","value":36},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"fe5787e45c7745e0","operationName":"[add] TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0ab31c0d4ea9a782"}],"startTime":1717262705916832,"duration":47,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"d7f723ea9ae2642403df19c6dff4aa7c"},{"key":"time_before","type":"int64","value":2},{"key":"mutation","type":"string","value":"[add] TopicBootstrapped"},{"key":"states_diff","type":"string","value":"+TopicBootstrapped -BootstrapChecking"},{"key":"time_after","type":"int64","value":4},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":5},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f861cfdc52150350","operationName":"PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"fcf8d9dbe6eea793"}],"startTime":1717262705912696,"duration":54,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"2081f8f1bb928e73430a50150058ec83"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"403219231db92745","operationName":"Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"8895fe4c20a914b9"}],"startTime":1717262705916807,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a963349568663286","operationName":"[remove] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705912752,"duration":43,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"e38b822eeb044373f4d70ddb19f45cdf"},{"key":"time_before","type":"int64","value":35},{"key":"mutation","type":"string","value":"[remove] PublishMessage"},{"key":"states_diff","type":"string","value":"-PublishMessage"},{"key":"time_after","type":"int64","value":36},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f98e97e080857c27","operationName":"PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"031e417fb7dda017"}],"startTime":1717262705912730,"duration":67,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"6d823cbbe1f8de73fc54b8b84db495e2"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"647df40aa3543f41","operationName":"BootstrapChecking","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"8895fe4c20a914b9"}],"startTime":1717262705916813,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"41e2b02a38aa6a1b","operationName":"BootstrappingTopicEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"024c2019f1215f11"}],"startTime":1717262705912922,"duration":11,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrappingTopicEnter"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"14bfaab14e942ce0","operationName":"BootstrapChecking","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"647df40aa3543f41"}],"startTime":1717262705916819,"duration":63,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"97b4f7cbdd458fa906e66301bc635559"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"888b8e1be32a7529","operationName":"TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"8895fe4c20a914b9"}],"startTime":1717262705916885,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"46e30066c604b23d","operationName":"BootstrappingTopicState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"024c2019f1215f11"}],"startTime":1717262705912952,"duration":3946,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrappingTopicState"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0114e353acc02086","operationName":"[add] TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1a6332b4daa47f30"}],"startTime":1717262705917002,"duration":450,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"1fa6d243f9c326759a21f7ba388f2b11"},{"key":"time_before","type":"int64","value":12},{"key":"mutation","type":"string","value":"[add] TopicBootstrapped"},{"key":"args.topic","type":"string","value":"foobar"},{"key":"states_diff","type":"string","value":"+TopicBootstrapped"},{"key":"time_after","type":"int64","value":14},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"4216ded5eb481736","operationName":"TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a73398ce083e803f"}],"startTime":1717262705884864,"duration":32593,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"fb3d83be096fa815b403a13b6e6f0846"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9240aab69a660101","operationName":"[add] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705917766,"duration":139,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"03cea52e6d79839a197373d36a7d130b"},{"key":"time_before","type":"int64","value":36},{"key":"mutation","type":"string","value":"[add] PublishMessage"},{"key":"states_diff","type":"string","value":"+PublishMessage"},{"key":"time_after","type":"int64","value":37},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"59c873e5281442c1","operationName":"BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"49604bf5fa2a05d8"}],"startTime":1717262705916920,"duration":704,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"b03ebe4f20ccee054e7e0de0f9db1c28"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"26d1f94dff0b468a","operationName":"BootstrappingTopicExit","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a726ec465c0059bc"}],"startTime":1717262705917529,"duration":20,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrappingTopicExit"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a726ec465c0059bc","operationName":"[remove] BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1a6332b4daa47f30"}],"startTime":1717262705917497,"duration":123,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"9fee7691a977dbe0c3f3ff08c967ffdc"},{"key":"time_before","type":"int64","value":14},{"key":"mutation","type":"string","value":"[remove] BootstrappingTopic"},{"key":"states_diff","type":"string","value":"-BootstrappingTopic"},{"key":"time_after","type":"int64","value":15},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"20600af5c1a3ce0b","operationName":"PublishMessageState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9240aab69a660101"}],"startTime":1717262705917838,"duration":29,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"024c2019f1215f11","operationName":"[add] BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1a6332b4daa47f30"}],"startTime":1717262705912873,"duration":4042,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"b03ebe4f20ccee054e7e0de0f9db1c28"},{"key":"time_before","type":"int64","value":11},{"key":"mutation","type":"string","value":"[add] BootstrappingTopic"},{"key":"args.topic","type":"string","value":"foobar"},{"key":"states_diff","type":"string","value":"+BootstrappingTopic"},{"key":"time_after","type":"int64","value":12},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1b6ea9135afbacdb","operationName":"PublishMessageEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9240aab69a660101"}],"startTime":1717262705917794,"duration":25,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"362cf2a4c4d2213f","operationName":"PublishMessageEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"e249dc5f8db28f93"}],"startTime":1717262705918641,"duration":24,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"8802d9b506c887d1","operationName":"TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"888b8e1be32a7529"}],"startTime":1717262705916888,"duration":1054,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"d7f723ea9ae2642403df19c6dff4aa7c"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"52d5c00a6ef460a9","operationName":"TopicBootstrappedState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0114e353acc02086"}],"startTime":1717262705917379,"duration":31,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"TopicBootstrappedState"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f8cebeca9787c875","operationName":"mach:ps-2-disc-bf-3-foobar","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"546cef4d80d5951a"}],"startTime":1717262705913021,"duration":4924,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"id","type":"string","value":"ps-2-disc-bf-3-foobar"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"e4cee93c379db1b3","operationName":"Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"403219231db92745"}],"startTime":1717262705916811,"duration":1128,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"97b4f7cbdd458fa906e66301bc635559"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9f1d8841aabab61a","operationName":"[remove] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705918873,"duration":93,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"0e2780c0591cfe97ad5a3438e21aa815"},{"key":"time_before","type":"int64","value":37},{"key":"mutation","type":"string","value":"[remove] PublishMessage"},{"key":"states_diff","type":"string","value":"-PublishMessage"},{"key":"time_after","type":"int64","value":38},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"af6fa00474020cb9","operationName":"[remove] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705917973,"duration":585,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"674e124df6b784e262475fd603b2e737"},{"key":"time_before","type":"int64","value":37},{"key":"mutation","type":"string","value":"[remove] PublishMessage"},{"key":"states_diff","type":"string","value":"-PublishMessage"},{"key":"time_after","type":"int64","value":38},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"35ce6579c8d3e1df","operationName":"PublishMessageEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1029135ec7e26e12"}],"startTime":1717262705918696,"duration":17,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"6f9a50401ae0551a","operationName":"[remove] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705918995,"duration":43,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"9f4ae09a78ba9d682c517427f6f4ef37"},{"key":"time_before","type":"int64","value":37},{"key":"mutation","type":"string","value":"[remove] PublishMessage"},{"key":"states_diff","type":"string","value":"-PublishMessage"},{"key":"time_after","type":"int64","value":38},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"37dae52e2a440bb7","operationName":"PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"cd741da4d202b261"}],"startTime":1717262705918816,"duration":154,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"c6113f0ad37217ae7ebbcfee735cc357"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"19f404252da86871","operationName":"PublishMessageState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"e249dc5f8db28f93"}],"startTime":1717262705918695,"duration":67,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b750be92fcbefc9e","operationName":"PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"031e417fb7dda017"}],"startTime":1717262705917914,"duration":656,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"03cea52e6d79839a197373d36a7d130b"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"e249dc5f8db28f93","operationName":"[add] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705918572,"duration":238,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"c6113f0ad37217ae7ebbcfee735cc357"},{"key":"time_before","type":"int64","value":36},{"key":"mutation","type":"string","value":"[add] PublishMessage"},{"key":"states_diff","type":"string","value":"+PublishMessage"},{"key":"time_after","type":"int64","value":37},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1029135ec7e26e12","operationName":"[add] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705918656,"duration":303,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"04461559c576a8598e18ca3387d23a81"},{"key":"time_before","type":"int64","value":36},{"key":"mutation","type":"string","value":"[add] PublishMessage"},{"key":"states_diff","type":"string","value":"+PublishMessage"},{"key":"time_after","type":"int64","value":37},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"810d97a9e33ceb66","operationName":"[add] Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1ccb9a77fca2c745"}],"startTime":1717262705921052,"duration":135,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"308b0aeab32a0ce25ae07bd0c018bcd0"},{"key":"time_before","type":"int64","value":0},{"key":"mutation","type":"string","value":"[add] Start"},{"key":"states_diff","type":"string","value":"+Start +BootstrapChecking"},{"key":"time_after","type":"int64","value":2},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":8},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"3ec4ffa80ca1a7c1","operationName":"BootstrappingTopicEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"3e001310d46aadf6"}],"startTime":1717262705919691,"duration":17,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrappingTopicEnter"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1ccb9a77fca2c745","operationName":"transitions","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9e02c24541c9955c"}],"startTime":1717262705919809,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-0-disc-bf-9-foobar"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"da14f8d72a9fa618","operationName":"PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"fcf8d9dbe6eea793"}],"startTime":1717262705918965,"duration":81,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"04461559c576a8598e18ca3387d23a81"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"2962ff44e5b6a043","operationName":"PublishMessageState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1029135ec7e26e12"}],"startTime":1717262705918740,"duration":65,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ba55348b373ca4d5","operationName":"BootstrapCheckingState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"810d97a9e33ceb66"}],"startTime":1717262705921144,"duration":32,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrapCheckingState"},{"key":"emitter","type":"string","value":"bootstrapFlow"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"780a9a76982df5db","operationName":"states","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9e02c24541c9955c"}],"startTime":1717262705919802,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"mach_id","type":"string","value":"ps-0-disc-bf-9-foobar"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"cb55ea7f3032fb1d","operationName":"BootstrappingTopicState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"3e001310d46aadf6"}],"startTime":1717262705919729,"duration":1536,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrappingTopicState"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"6f4d2f4055c2069c","operationName":"StartState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"810d97a9e33ceb66"}],"startTime":1717262705921105,"duration":29,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"StartState"},{"key":"emitter","type":"string","value":"bootstrapFlow"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"3496af339b56f3bc","operationName":"[add] TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a47565b8219f873b"}],"startTime":1717262705921383,"duration":99,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"93f5b8278ae171b8f82a5e9dede41159"},{"key":"time_before","type":"int64","value":28},{"key":"mutation","type":"string","value":"[add] TopicBootstrapped"},{"key":"args.topic","type":"string","value":"foobar"},{"key":"states_diff","type":"string","value":"+TopicBootstrapped"},{"key":"time_after","type":"int64","value":30},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1bfea74ddf23e029","operationName":"BootstrapChecking","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"e2d330d7c71d9926"}],"startTime":1717262705921199,"duration":51,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"308b0aeab32a0ce25ae07bd0c018bcd0"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"3e001310d46aadf6","operationName":"[add] BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a47565b8219f873b"}],"startTime":1717262705919256,"duration":2032,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"240361403b6b125f08f0fabc3c2db835"},{"key":"time_before","type":"int64","value":27},{"key":"mutation","type":"string","value":"[add] BootstrappingTopic"},{"key":"args.topic","type":"string","value":"foobar"},{"key":"states_diff","type":"string","value":"+BootstrappingTopic"},{"key":"time_after","type":"int64","value":28},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"93a4dd415af78d65","operationName":"TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"780a9a76982df5db"}],"startTime":1717262705921252,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"256e8ce7bfd1b414","operationName":"TopicBootstrappedState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"3496af339b56f3bc"}],"startTime":1717262705921440,"duration":24,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"TopicBootstrappedState"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"e2d330d7c71d9926","operationName":"BootstrapChecking","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"780a9a76982df5db"}],"startTime":1717262705921197,"duration":0,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"3c3d5112adf07d92","operationName":"TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d12d683e8445bad4"}],"startTime":1717262705908222,"duration":13262,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"58a1c5db0481dc78d9e6fa9b0fa5bb63"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"8e4264f000b8d4be","operationName":"[remove] BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a47565b8219f873b"}],"startTime":1717262705921499,"duration":69,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"1360af1133f02024cb1e3bc5490dfc1a"},{"key":"time_before","type":"int64","value":30},{"key":"mutation","type":"string","value":"[remove] BootstrappingTopic"},{"key":"states_diff","type":"string","value":"-BootstrappingTopic"},{"key":"time_after","type":"int64","value":31},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":3},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"48a2d903b9388101","operationName":"BootstrappingTopicExit","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"8e4264f000b8d4be"}],"startTime":1717262705921522,"duration":9,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"BootstrappingTopicExit"},{"key":"emitter","type":"string","value":"discover"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d362ba3674419481","operationName":"PublishMessageEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9f4be8e07aaddae2"}],"startTime":1717262705921776,"duration":12,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a27065e695491f88","operationName":"[add] TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1ccb9a77fca2c745"}],"startTime":1717262705921211,"duration":37,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"3ad32214e90757c8ee1a9fa178c3de11"},{"key":"time_before","type":"int64","value":2},{"key":"mutation","type":"string","value":"[add] TopicBootstrapped"},{"key":"states_diff","type":"string","value":"+TopicBootstrapped -BootstrapChecking"},{"key":"time_after","type":"int64","value":4},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":5},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"2bf2b853e321acd7","operationName":"Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"780a9a76982df5db"}],"startTime":1717262705921191,"duration":1,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a8aabac72743bf56","operationName":"Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"2bf2b853e321acd7"}],"startTime":1717262705921195,"duration":417,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"308b0aeab32a0ce25ae07bd0c018bcd0"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9e02c24541c9955c","operationName":"mach:ps-0-disc-bf-9-foobar","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"881d141acaa59733"}],"startTime":1717262705919777,"duration":1838,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"id","type":"string","value":"ps-0-disc-bf-9-foobar"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"2544974a307cd365","operationName":"BootstrappingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"16f8c5c868c37c27"}],"startTime":1717262705921293,"duration":276,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"240361403b6b125f08f0fabc3c2db835"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a15f6dfb8d9b0741","operationName":"TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"93a4dd415af78d65"}],"startTime":1717262705921254,"duration":359,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"3ad32214e90757c8ee1a9fa178c3de11"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"6c03cda28a557330","operationName":"PublishMessageState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9f4be8e07aaddae2"}],"startTime":1717262705921802,"duration":12,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"66eb5824d6e9693b","operationName":"[remove] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705921856,"duration":27,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"cda87f93631b285f1a24aed0f3aee724"},{"key":"time_before","type":"int64","value":39},{"key":"mutation","type":"string","value":"[remove] PublishMessage"},{"key":"states_diff","type":"string","value":"-PublishMessage"},{"key":"time_after","type":"int64","value":40},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"9f4be8e07aaddae2","operationName":"[add] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0e1e4e9f54c2bdfa"}],"startTime":1717262705921757,"duration":80,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"ae6310efbcdd48d41915718bf7bdd808"},{"key":"time_before","type":"int64","value":38},{"key":"mutation","type":"string","value":"[add] PublishMessage"},{"key":"states_diff","type":"string","value":"+PublishMessage"},{"key":"time_after","type":"int64","value":39},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"5b2c5fd4a6a52102","operationName":"PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"cd741da4d202b261"}],"startTime":1717262705922344,"duration":49,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"9765e2614d503d05349356886cdfb021"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"66e0870af7e547ab","operationName":"[add] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705922263,"duration":77,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"9765e2614d503d05349356886cdfb021"},{"key":"time_before","type":"int64","value":38},{"key":"mutation","type":"string","value":"[add] PublishMessage"},{"key":"states_diff","type":"string","value":"+PublishMessage"},{"key":"time_after","type":"int64","value":39},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ed69b3b3e444b4af","operationName":"rand msgs","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"fd6161db1d657ad6"}],"startTime":1717262705877643,"duration":44720,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"52f065e6a98a3d35","operationName":"PublishMessageEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"e1d0449649099ef8"}],"startTime":1717262705922273,"duration":15,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"3a770f6804abbb7c","operationName":"[remove] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"32cadea5e4330537"}],"startTime":1717262705922360,"duration":31,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"1df89542d02f76fa777aa75850d1f8f2"},{"key":"time_before","type":"int64","value":39},{"key":"mutation","type":"string","value":"[remove] PublishMessage"},{"key":"states_diff","type":"string","value":"-PublishMessage"},{"key":"time_after","type":"int64","value":40},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"df9841787839bcec","operationName":"PublishMessageState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"66e0870af7e547ab"}],"startTime":1717262705922300,"duration":19,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f79e3ac353a8a778","operationName":"PublishMessageState","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"e1d0449649099ef8"}],"startTime":1717262705922300,"duration":32,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageState"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"8da8ad81f811f9aa","operationName":"PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"fcf8d9dbe6eea793"}],"startTime":1717262705921841,"duration":43,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"ae6310efbcdd48d41915718bf7bdd808"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"8f9a0ce7d9d6ff32","operationName":"Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"144037c1afcec3ce"}],"startTime":1717262705768300,"duration":154217,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"0452c84fb23d80f1de3558eac3046db4"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"34ea00d39795be20","operationName":"mach:ps-0","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b0355d592c0442a2"}],"startTime":1717262705761149,"duration":161301,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"id","type":"string","value":"ps-0"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"e1d0449649099ef8","operationName":"[add] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705922234,"duration":117,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"f1f1f612ded3ce2d4c45130719837446"},{"key":"time_before","type":"int64","value":38},{"key":"mutation","type":"string","value":"[add] PublishMessage"},{"key":"states_diff","type":"string","value":"+PublishMessage"},{"key":"time_after","type":"int64","value":39},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":4},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"151800292830e65b","operationName":"PublishMessageEnter","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"66e0870af7e547ab"}],"startTime":1717262705922283,"duration":8,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"name","type":"string","value":"PublishMessageEnter"},{"key":"emitter","type":"string","value":"PubSub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0a7fe9efef8ab160","operationName":"[remove] PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"23777b5d2f6206ba"}],"startTime":1717262705922370,"duration":28,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"7caf7a46ba4ce7e3a94134c6d640e851"},{"key":"time_before","type":"int64","value":39},{"key":"mutation","type":"string","value":"[remove] PublishMessage"},{"key":"states_diff","type":"string","value":"-PublishMessage"},{"key":"time_after","type":"int64","value":40},{"key":"accepted","type":"bool","value":true},{"key":"steps_count","type":"int64","value":2},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"dd1da5f0e2a463fd","operationName":"mach:ps-1","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0ae0eb6488d3f1f3"}],"startTime":1717262705765080,"duration":157396,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"id","type":"string","value":"ps-1"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"224747f6268595df","operationName":"PublishMessage","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"031e417fb7dda017"}],"startTime":1717262705922354,"duration":45,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"f1f1f612ded3ce2d4c45130719837446"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"98d86777aab009c1","operationName":"mach:ps-2","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"83dad7c121cb9105"}],"startTime":1717262705766812,"duration":155686,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"id","type":"string","value":"ps-2"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"26a60b2519f3219c","operationName":"TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a73398ce083e803f"}],"startTime":1717262705917466,"duration":5049,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"1fa6d243f9c326759a21f7ba388f2b11"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"20f8bba7e7ac0a03","operationName":"PoolTimer","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"36e3c92ca7e702e1"}],"startTime":1717262705768306,"duration":154212,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"0452c84fb23d80f1de3558eac3046db4"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c2880a5fc08ef781","operationName":"AdvertisingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"12d58a036392513d"}],"startTime":1717262705771942,"duration":150592,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"c64e4ce37139e5e331e751503ffd6579"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d8dc5e5c1bbd25ad","operationName":"TopicDiscovered","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b7f9002d04314828"}],"startTime":1717262705770115,"duration":152449,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"ad0ef63e690f4b30e187a0e2040484ed"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"4016d057443c05a5","operationName":"mach:ps-2-disc","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ef6ee7c2084c661e"}],"startTime":1717262705767522,"duration":155002,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"id","type":"string","value":"ps-2-disc"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"4a4239e3c56fc819","operationName":"TopicDiscovered","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"f898e8ef3aae4b55"}],"startTime":1717262705770928,"duration":151591,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"a2f2e6ce36e6fd0f824d191661bdd410"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"49951a12ba6f1b92","operationName":"TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"d12d683e8445bad4"}],"startTime":1717262705921487,"duration":1043,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"93f5b8278ae171b8f82a5e9dede41159"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ace08cc9a2dd865f","operationName":"AdvertisingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"5a9fd4633372f4fc"}],"startTime":1717262705770045,"duration":152475,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"e9bd2184add3e48e3c051f8bea29a726"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"dadf4ed678b3e21c","operationName":"Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"a88e6fa22f296b08"}],"startTime":1717262705764545,"duration":157987,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"76e0ac79d2916716c248965eb6cdd14b"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"2254d2fc5cfe3ee6","operationName":"TopicDiscovered","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"abff6370a8cb8c29"}],"startTime":1717262705774738,"duration":147798,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"dc8ee003f4b8f65c535364cde441580e"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b20253bc2207a232","operationName":"mach:ps-0-disc","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"1960bdbf49f493cd"}],"startTime":1717262705763728,"duration":158812,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"id","type":"string","value":"ps-0-disc"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"fd6161db1d657ad6","operationName":"testcase","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"17e8ba3651b7c701"}],"startTime":1717262705749782,"duration":173197,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"8ff496c5ee9f5050","operationName":"PoolTimer","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"daea4ce841bf5fef"}],"startTime":1717262705766505,"duration":156046,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"e340c9b43c97a6feb574a52b57d59336"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"6227980ad6e33ca8","operationName":"PoolTimer","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"81c88944adc11de1"}],"startTime":1717262705764548,"duration":157985,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"76e0ac79d2916716c248965eb6cdd14b"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"95448673cc306b20","operationName":"TopicBootstrapped","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"3cd8aeeb719e3faa"}],"startTime":1717262705911760,"duration":10806,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"eb81c9316886350e7e49b412fa648741"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"ce8927c488b509c6","operationName":"mach:ps-1-disc","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"6f4a21dcbdc58806"}],"startTime":1717262705765780,"duration":156790,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"id","type":"string","value":"ps-1-disc"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"244474acc7ab30d3","operationName":"Start","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"c17362ec6070fc18"}],"startTime":1717262705766503,"duration":156046,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"e340c9b43c97a6feb574a52b57d59336"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b0355d592c0442a2","operationName":"host:1","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"17e8ba3651b7c701"}],"startTime":1717262705761067,"duration":161914,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"host_id","type":"string","value":"12D3KooWC4tgrsYRyYkKiqSowkArL5eagvkVnpjYxcuHG8NB72UC"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"83dad7c121cb9105","operationName":"host:3","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"17e8ba3651b7c701"}],"startTime":1717262705766746,"duration":156236,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"host_id","type":"string","value":"12D3KooWEEutJgmU8RioyW2EDfwkTwbUW8irYQCEeuAtJ3BckmHM"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"17e8ba3651b7c701","operationName":"psmon","references":[],"startTime":1717262705749575,"duration":173408,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"0ae0eb6488d3f1f3","operationName":"host:2","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"17e8ba3651b7c701"}],"startTime":1717262705765002,"duration":157980,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"host_id","type":"string","value":"12D3KooWE2VH9ZYTkKZcyvRwcauvu63wABJgFztRx6PVxm95QfMd"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null},{"traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"b414bd8b5a5b40f7","operationName":"AdvertisingTopic","references":[{"refType":"CHILD_OF","traceID":"107a885d952c18cc2ece2b2fa1ecd521","spanID":"50da202aef2e69f5"}],"startTime":1717262705769162,"duration":153403,"tags":[{"key":"otel.library.name","type":"string","value":"github.com/libp2p/go-libp2p-pubsub"},{"key":"tx_id","type":"string","value":"9ebae09ea5be2979a599cd9047b79345"},{"key":"span.kind","type":"string","value":"internal"},{"key":"internal.span.format","type":"string","value":"otlp"}],"logs":[],"processID":"p1","warnings":null}],"processes":{"p1":{"serviceName":"ps","tags":[]}},"warnings":null}],"total":0,"limit":0,"offset":0,"errors":null}
\ No newline at end of file
diff --git a/assets/bench.dark.jpg b/assets/bench.dark.jpg
new file mode 100644
index 0000000..487dfeb
Binary files /dev/null and b/assets/bench.dark.jpg differ
diff --git a/assets/bench.light.png b/assets/bench.light.png
new file mode 100644
index 0000000..7f37bae
Binary files /dev/null and b/assets/bench.light.png differ
diff --git a/assets/grafana-mach-sim,sim-p1.json b/assets/grafana-mach-sim,sim-p1.json
new file mode 100644
index 0000000..8546e87
--- /dev/null
+++ b/assets/grafana-mach-sim,sim-p1.json
@@ -0,0 +1,719 @@
+{
+ "__inputs": [],
+ "annotations": {
+ "list": []
+ },
+ "description": "AsyncMachine internals gathered by pkg/telemetry",
+ "editable": true,
+ "gnetId": null,
+ "graphTooltip": 0,
+ "hideControls": false,
+ "id": null,
+ "links": [],
+ "panels": [
+ {
+ "cacheTimeout": null,
+ "collapsed": false,
+ "datasource": null,
+ "description": null,
+ "editable": true,
+ "error": false,
+ "fieldConfig": {
+ "defaults": {
+ "thresholds": {
+ "mode": "absolute",
+ "steps": []
+ }
+ }
+ },
+ "gridPos": {
+ "h": 1,
+ "w": 24,
+ "x": 0,
+ "y": 0
+ },
+ "height": null,
+ "hideTimeOverride": false,
+ "id": 1,
+ "interval": null,
+ "links": [],
+ "maxDataPoints": 100,
+ "maxPerRow": null,
+ "minSpan": null,
+ "panels": [],
+ "repeat": null,
+ "repeatDirection": null,
+ "span": null,
+ "targets": [],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "Machine: $IDS",
+ "transformations": [],
+ "transparent": false,
+ "type": "row"
+ },
+ {
+ "cacheTimeout": null,
+ "datasource": "prometheus",
+ "description": null,
+ "editable": true,
+ "error": false,
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "axisSoftMax": null,
+ "axisSoftMin": null,
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 0,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "log": 2,
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "spanNulls": false,
+ "stacking": {},
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "decimals": null,
+ "mappings": [],
+ "max": null,
+ "min": null,
+ "thresholds": {
+ "mode": "absolute",
+ "steps": []
+ },
+ "unit": ""
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 1
+ },
+ "height": null,
+ "hideTimeOverride": false,
+ "id": 2,
+ "interval": "5s",
+ "links": [],
+ "maxDataPoints": 100,
+ "maxPerRow": null,
+ "minSpan": null,
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom"
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "repeat": null,
+ "repeatDirection": null,
+ "span": null,
+ "targets": [
+ {
+ "datasource": null,
+ "expr": "mach_$IDS_queue_size",
+ "format": "time_series",
+ "hide": false,
+ "instant": false,
+ "interval": "",
+ "intervalFactor": 2,
+ "legendFormat": "Queue size",
+ "metric": "",
+ "query": "mach_$IDS_queue_size",
+ "refId": "",
+ "step": 10,
+ "target": ""
+ },
+ {
+ "datasource": null,
+ "expr": "mach_$IDS_tx_tick",
+ "format": "time_series",
+ "hide": false,
+ "instant": false,
+ "interval": "",
+ "intervalFactor": 2,
+ "legendFormat": "Tx ticks",
+ "metric": "",
+ "query": "mach_$IDS_tx_tick",
+ "refId": "",
+ "step": 10,
+ "target": ""
+ },
+ {
+ "datasource": null,
+ "expr": "mach_$IDS_steps_amount",
+ "format": "time_series",
+ "hide": false,
+ "instant": false,
+ "interval": "",
+ "intervalFactor": 2,
+ "legendFormat": "Number of tx steps",
+ "metric": "",
+ "query": "mach_$IDS_steps_amount",
+ "refId": "",
+ "step": 10,
+ "target": ""
+ },
+ {
+ "datasource": null,
+ "expr": "mach_$IDS_handlers_amount",
+ "format": "time_series",
+ "hide": false,
+ "instant": false,
+ "interval": "",
+ "intervalFactor": 2,
+ "legendFormat": "Called handlers",
+ "metric": "",
+ "query": "mach_$IDS_handlers_amount",
+ "refId": "",
+ "step": 10,
+ "target": ""
+ },
+ {
+ "datasource": null,
+ "expr": "mach_$IDS_states_added",
+ "format": "time_series",
+ "hide": false,
+ "instant": false,
+ "interval": "",
+ "intervalFactor": 2,
+ "legendFormat": "States added",
+ "metric": "",
+ "query": "mach_$IDS_states_added",
+ "refId": "",
+ "step": 10,
+ "target": ""
+ },
+ {
+ "datasource": null,
+ "expr": "mach_$IDS_states_removed",
+ "format": "time_series",
+ "hide": false,
+ "instant": false,
+ "interval": "",
+ "intervalFactor": 2,
+ "legendFormat": "States removed",
+ "metric": "",
+ "query": "mach_$IDS_states_removed",
+ "refId": "",
+ "step": 10,
+ "target": ""
+ },
+ {
+ "datasource": null,
+ "expr": "mach_$IDS_touched",
+ "format": "time_series",
+ "hide": false,
+ "instant": false,
+ "interval": "",
+ "intervalFactor": 2,
+ "legendFormat": "States touched",
+ "metric": "",
+ "query": "mach_$IDS_touched",
+ "refId": "",
+ "step": 10,
+ "target": ""
+ }
+ ],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "Transition changes",
+ "transformations": [],
+ "transparent": false,
+ "type": "timeseries"
+ },
+ {
+ "cacheTimeout": null,
+ "datasource": "prometheus",
+ "description": null,
+ "editable": true,
+ "error": false,
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "axisSoftMax": null,
+ "axisSoftMin": null,
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 0,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "log": 2,
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "spanNulls": false,
+ "stacking": {},
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "decimals": null,
+ "mappings": [],
+ "max": null,
+ "min": null,
+ "thresholds": {
+ "mode": "absolute",
+ "steps": []
+ },
+ "unit": ""
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 1
+ },
+ "height": null,
+ "hideTimeOverride": false,
+ "id": 3,
+ "interval": "5s",
+ "links": [],
+ "maxDataPoints": 100,
+ "maxPerRow": null,
+ "minSpan": null,
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom"
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "repeat": null,
+ "repeatDirection": null,
+ "span": null,
+ "targets": [
+ {
+ "datasource": null,
+ "expr": "mach_$IDS_ref_states_amount",
+ "format": "time_series",
+ "hide": false,
+ "instant": false,
+ "interval": "",
+ "intervalFactor": 2,
+ "legendFormat": "States referenced in relations",
+ "metric": "",
+ "query": "mach_$IDS_ref_states_amount",
+ "refId": "",
+ "step": 10,
+ "target": ""
+ },
+ {
+ "datasource": null,
+ "expr": "mach_$IDS_relations_amount",
+ "format": "time_series",
+ "hide": false,
+ "instant": false,
+ "interval": "",
+ "intervalFactor": 2,
+ "legendFormat": "Relations",
+ "metric": "",
+ "query": "mach_$IDS_relations_amount",
+ "refId": "",
+ "step": 10,
+ "target": ""
+ },
+ {
+ "datasource": null,
+ "expr": "mach_$IDS_states_active_amount",
+ "format": "time_series",
+ "hide": false,
+ "instant": false,
+ "interval": "",
+ "intervalFactor": 2,
+ "legendFormat": "States active",
+ "metric": "",
+ "query": "mach_$IDS_states_active_amount",
+ "refId": "",
+ "step": 10,
+ "target": ""
+ },
+ {
+ "datasource": null,
+ "expr": "mach_$IDS_states_inactive_amount",
+ "format": "time_series",
+ "hide": false,
+ "instant": false,
+ "interval": "",
+ "intervalFactor": 2,
+ "legendFormat": "States inactive",
+ "metric": "",
+ "query": "mach_$IDS_states_inactive_amount",
+ "refId": "",
+ "step": 10,
+ "target": ""
+ }
+ ],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "Machine states",
+ "transformations": [],
+ "transparent": false,
+ "type": "timeseries"
+ },
+ {
+ "cacheTimeout": null,
+ "datasource": "prometheus",
+ "description": null,
+ "editable": true,
+ "error": false,
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "axisSoftMax": null,
+ "axisSoftMin": null,
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 0,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "log": 2,
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "spanNulls": false,
+ "stacking": {},
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "decimals": null,
+ "mappings": [],
+ "max": null,
+ "min": null,
+ "thresholds": {
+ "mode": "absolute",
+ "steps": []
+ },
+ "unit": ""
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 9
+ },
+ "height": null,
+ "hideTimeOverride": false,
+ "id": 4,
+ "interval": "5s",
+ "links": [],
+ "maxDataPoints": 100,
+ "maxPerRow": null,
+ "minSpan": null,
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom"
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "repeat": null,
+ "repeatDirection": null,
+ "span": null,
+ "targets": [
+ {
+ "datasource": null,
+ "expr": "mach_$IDS_tx_time / 1000",
+ "format": "time_series",
+ "hide": false,
+ "instant": false,
+ "interval": "",
+ "intervalFactor": 2,
+ "legendFormat": "Tx time (ms)",
+ "metric": "",
+ "query": "mach_$IDS_tx_time / 1000",
+ "refId": "",
+ "step": 10,
+ "target": ""
+ }
+ ],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "Transition time",
+ "transformations": [],
+ "transparent": false,
+ "type": "timeseries"
+ },
+ {
+ "cacheTimeout": null,
+ "datasource": "prometheus",
+ "description": null,
+ "editable": true,
+ "error": false,
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "thresholds"
+ },
+ "custom": {
+ "fillOpacity": 80,
+ "lineWidth": 0
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": []
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 9
+ },
+ "height": null,
+ "hideTimeOverride": false,
+ "id": 5,
+ "interval": "5s",
+ "links": [],
+ "maxDataPoints": 100,
+ "maxPerRow": null,
+ "minSpan": null,
+ "options": {
+ "bucketOffset": 0,
+ "combine": false,
+ "legend": {
+ "displayMode": "list",
+ "placement": "bottom"
+ }
+ },
+ "repeat": null,
+ "repeatDirection": null,
+ "span": null,
+ "targets": [
+ {
+ "datasource": null,
+ "expr": "mach_$IDS_tx_time / 1000",
+ "format": "time_series",
+ "hide": false,
+ "instant": false,
+ "interval": "",
+ "intervalFactor": 2,
+ "legendFormat": "Tx time (ms)",
+ "metric": "",
+ "query": "mach_$IDS_tx_time / 1000",
+ "refId": "",
+ "step": 10,
+ "target": ""
+ }
+ ],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "Average transition time",
+ "transformations": [],
+ "transparent": false,
+ "type": "histogram"
+ },
+ {
+ "cacheTimeout": null,
+ "datasource": "prometheus",
+ "description": null,
+ "editable": true,
+ "error": false,
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "axisSoftMax": null,
+ "axisSoftMin": null,
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 0,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "log": 2,
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "spanNulls": false,
+ "stacking": {},
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "decimals": null,
+ "mappings": [],
+ "max": null,
+ "min": null,
+ "thresholds": {
+ "mode": "absolute",
+ "steps": []
+ },
+ "unit": ""
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 17
+ },
+ "height": null,
+ "hideTimeOverride": false,
+ "id": 6,
+ "interval": "5s",
+ "links": [],
+ "maxDataPoints": 100,
+ "maxPerRow": null,
+ "minSpan": null,
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom"
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "repeat": null,
+ "repeatDirection": null,
+ "span": null,
+ "targets": [
+ {
+ "datasource": null,
+ "expr": "mach_$IDS_exceptions_count",
+ "format": "time_series",
+ "hide": false,
+ "instant": false,
+ "interval": "",
+ "intervalFactor": 2,
+ "legendFormat": "Exceptions",
+ "metric": "",
+ "query": "mach_$IDS_exceptions_count",
+ "refId": "",
+ "step": 10,
+ "target": ""
+ }
+ ],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "Exceptions",
+ "transformations": [],
+ "transparent": false,
+ "type": "timeseries"
+ }
+ ],
+ "refresh": "5s",
+ "rows": [],
+ "schemaVersion": 12,
+ "sharedCrosshair": false,
+ "style": "dark",
+ "tags": [],
+ "templating": {
+ "list": []
+ },
+ "time": {
+ "from": "now-1h",
+ "to": "now"
+ },
+ "timepicker": {
+ "hidden": false,
+ "nowDelay": null,
+ "refresh_intervals": [
+ "5s",
+ "10s",
+ "30s",
+ "1m",
+ "5m",
+ "15m",
+ "30m",
+ "1h",
+ "2h",
+ "1d"
+ ],
+ "time_options": [
+ "5m",
+ "15m",
+ "1h",
+ "6h",
+ "12h",
+ "24h",
+ "2d",
+ "7d",
+ "30d"
+ ]
+ },
+ "timezone": "browser",
+ "title": "Machine Dashboard: $IDS",
+ "uid": null,
+ "version": 0
+}
diff --git a/assets/otel-jaeger.dark.png b/assets/otel-jaeger.dark.png
new file mode 100644
index 0000000..8f71a24
Binary files /dev/null and b/assets/otel-jaeger.dark.png differ
diff --git a/assets/otel-jaeger.light.png b/assets/otel-jaeger.light.png
new file mode 100644
index 0000000..526bacb
Binary files /dev/null and b/assets/otel-jaeger.light.png differ
diff --git a/assets/prometheus-grafana.dark.png b/assets/prometheus-grafana.dark.png
new file mode 100644
index 0000000..49bf854
Binary files /dev/null and b/assets/prometheus-grafana.dark.png differ
diff --git a/assets/prometheus-grafana.light.png b/assets/prometheus-grafana.light.png
new file mode 100644
index 0000000..71fd714
Binary files /dev/null and b/assets/prometheus-grafana.light.png differ
diff --git a/assets/sim-grafana.dark.png b/assets/sim-grafana.dark.png
new file mode 100644
index 0000000..4b3056c
Binary files /dev/null and b/assets/sim-grafana.dark.png differ
diff --git a/assets/sim-grafana.light.png b/assets/sim-grafana.light.png
new file mode 100644
index 0000000..a1d2e4f
Binary files /dev/null and b/assets/sim-grafana.light.png differ
diff --git a/config/.mdl_style.rb b/config/.mdl_style.rb
new file mode 100644
index 0000000..ca076f4
--- /dev/null
+++ b/config/.mdl_style.rb
@@ -0,0 +1,12 @@
+all
+rule 'MD013', :line_length => 120
+# list style
+rule 'MD029', :style => :ordered
+# dollar sign
+exclude_rule 'MD014'
+# HTML blocks
+exclude_rule 'MD033'
+# block quotes
+exclude_rule 'MD028'
+# ???
+exclude_rule 'MD007'
diff --git a/.mdlrc b/config/.mdlrc
similarity index 100%
rename from .mdlrc
rename to config/.mdlrc
diff --git a/config/terminalizer.yml b/config/terminalizer.yml
new file mode 100644
index 0000000..c571559
--- /dev/null
+++ b/config/terminalizer.yml
@@ -0,0 +1,108 @@
+# Specify a command to be executed
+# like `/bin/bash -l`, `ls`, or any other commands
+# the default is bash for Linux
+# or powershell.exe for Windows
+command: task demo-run
+
+# Specify the current working directory path
+# the default is the current working directory path
+cwd: null
+
+# Export additional ENV variables
+env:
+ recording: true
+
+# Explicitly set the number of columns
+# or use `auto` to take the current
+# number of columns of your shell
+cols: auto
+
+# Explicitly set the number of rows
+# or use `auto` to take the current
+# number of rows of your shell
+rows: auto
+
+# Amount of times to repeat GIF
+# If value is -1, play once
+# If value is 0, loop indefinitely
+# If value is a positive number, loop n times
+repeat: 0
+
+# Quality
+# 1 - 100
+quality: 100
+
+# Delay between frames in ms
+# If the value is `auto` use the actual recording delays
+frameDelay: auto
+
+# Maximum delay between frames in ms
+# Ignored if the `frameDelay` isn't set to `auto`
+# Set to `auto` to prevent limiting the max idle time
+maxIdleTime: 2000
+
+# The surrounding frame box
+# The `type` can be null, window, floating, or solid`
+# To hide the title use the value null
+# Don't forget to add a backgroundColor style with a null as type
+frameBox:
+ type: null
+ title: null
+ style:
+ border: 0px black solid
+ background: black
+ # boxShadow: none
+ # margin: 0px
+
+# Add a watermark image to the rendered gif
+# You need to specify an absolute path for
+# the image on your machine or a URL, and you can also
+# add your own CSS styles
+watermark:
+ imagePath: null
+ style:
+ position: absolute
+ right: 15px
+ bottom: 15px
+ width: 100px
+ opacity: 0.9
+
+# Cursor style can be one of
+# `block`, `underline`, or `bar`
+cursorStyle: block
+
+# Font family
+# You can use any font that is installed on your machine
+# in CSS-like syntax
+fontFamily: "Liberation Mono, Monaco, Lucida Console, Ubuntu Mono, Monospace"
+
+# The size of the font
+fontSize: 12
+
+# The height of lines
+lineHeight: 1
+
+# The spacing between letters
+letterSpacing: 0
+
+# Theme
+theme:
+ background: "transparent"
+ foreground: "#afafaf"
+ cursor: "#c7c7c7"
+ black: "#232628"
+ red: "#fc4384"
+ green: "#b3e33b"
+ yellow: "#ffa727"
+ blue: "#75dff2"
+ magenta: "#ae89fe"
+ cyan: "#708387"
+ white: "#d5d5d0"
+ brightBlack: "#626566"
+ brightRed: "#ff7fac"
+ brightGreen: "#c8ed71"
+ brightYellow: "#ebdf86"
+ brightBlue: "#75dff2"
+ brightMagenta: "#ae89fe"
+ brightCyan: "#b1c6ca"
+ brightWhite: "#f9f9f4"
diff --git a/deployments/web-metrics/docker-compose.yml b/deployments/web-metrics/docker-compose.yml
new file mode 100644
index 0000000..6609705
--- /dev/null
+++ b/deployments/web-metrics/docker-compose.yml
@@ -0,0 +1,43 @@
+services:
+
+ prometheus:
+ image: prom/prometheus:v2.37.9
+ container_name: prometheus
+ user: root
+ ports:
+ - "9090:9090"
+ command:
+ - '--config.file=/etc/prometheus/prometheus.yaml'
+ volumes:
+ - prometheus.yaml:/etc/prometheus/prometheus.yaml:ro
+ restart: unless-stopped
+
+ grafana:
+ image: grafana/grafana-oss:latest
+ container_name: grafana
+ ports:
+ - "3000:3000"
+ volumes:
+ - grafana_data:/var/lib/grafana
+ restart: unless-stopped
+ #password: root123
+
+ pushgateway:
+ image: prom/pushgateway
+ container_name: pushgateway
+ ports:
+ - "9091:9091"
+ restart: unless-stopped
+
+ jaeger:
+ image: jaegertracing/all-in-one:1.26
+ container_name: jaeger
+ environment:
+ - COLLECTOR_OTLP_ENABLED=true
+ ports:
+ - "16686:16686"
+ - "4317:4317"
+ restart: unless-stopped
+
+volumes:
+ grafana_data: {}
\ No newline at end of file
diff --git a/deployments/web-metrics/prometheus.yaml b/deployments/web-metrics/prometheus.yaml
new file mode 100644
index 0000000..307d583
--- /dev/null
+++ b/deployments/web-metrics/prometheus.yaml
@@ -0,0 +1,9 @@
+global:
+ scrape_interval: 1s
+
+scrape_configs:
+ - job_name: 'pushgateway'
+ honor_labels: true
+ scrape_interval: 5s
+ static_configs:
+ - targets: [ 'pushgateway:9091' ]
diff --git a/docs/cookbook.md b/docs/cookbook.md
new file mode 100644
index 0000000..8b57058
--- /dev/null
+++ b/docs/cookbook.md
@@ -0,0 +1,609 @@
+# Cookbook
+
+Cookbook for asyncmachine-go contains numerous snippets of common patterns.
+
+## ToC
+
+
+
+- [Cookbook](#cookbook)
+ - [ToC](#toc)
+ - [Activation handler with negotiation](#activation-handler-with-negotiation)
+ - [De-activation handler with negotiation](#de-activation-handler-with-negotiation)
+ - [State to state handlers](#state-to-state-handlers)
+ - [Debugging a machine](#debugging-a-machine)
+ - [Simple logging](#simple-logging)
+ - [Custom logging](#custom-logging)
+ - [Minimal machine init](#minimal-machine-init)
+ - [Common machine init](#common-machine-init)
+ - [Wait for a state activation](#wait-for-a-state-activation)
+ - [Wait for a state activation with argument values](#wait-for-a-state-activation-with-argument-values)
+ - [Wait for a state de-activation](#wait-for-a-state-de-activation)
+ - [Wait for a specific machine time](#wait-for-a-specific-machine-time)
+ - [Wait for a relative state tick](#wait-for-a-relative-state-tick)
+ - [Wait for a specific state tick](#wait-for-a-specific-state-tick)
+ - [Synchronous state (single)](#synchronous-state-single)
+ - [Asynchronous state (double)](#asynchronous-state-double)
+ - [Asynchronous boolean state (triple)](#asynchronous-boolean-state-triple)
+ - [Full asynchronous boolean state (quadruple)](#full-asynchronous-boolean-state-quadruple)
+ - [Input `Multi` state](#input-multi-state)
+ - [Self removal state](#self-removal-state)
+ - [State context](#state-context)
+ - [Step context](#step-context)
+ - [Event handlers](#event-handlers)
+ - [Channel responses via arguments](#channel-responses-via-arguments)
+ - [Equivalent of `select` write with `default`](#equivalent-of-select-write-with-default)
+ - [Custom exception handler](#custom-exception-handler)
+ - [Verify states at runtime](#verify-states-at-runtime)
+ - [Typesafe states pkg template](#typesafe-states-pkg-template)
+ - [State Mutex Groups](#state-mutex-groups)
+ - [Transition-aware handler](#transition-aware-handler)
+ - [Batch data into a single transition](#batch-data-into-a-single-transition)
+ - [Switch a state group](#switch-a-state-group)
+ - [DiffStates to navigate the flow](#diffstates-to-navigate-the-flow)
+ - [Pass data from a negotiation handler to the final handler](#pass-data-from-a-negotiation-handler-to-the-final-handler)
+ - [Check if a group od states is active](#check-if-a-group-od-states-is-active)
+
+
+
+## Activation handler with negotiation
+
+```go
+// negotiation handler
+func (h *Handlers) NameEnter(e *am.Event) bool {}
+// final handler
+func (h *Handlers) NameState(e *am.Event) {}
+```
+
+## De-activation handler with negotiation
+
+```go
+// negotiation handler
+func (h *Handlers) NameExit(e *am.Event) bool {}
+// final handler
+func (h *Handlers) NameEnd(e *am.Event) {}
+```
+
+## State to state handlers
+
+```go
+// all state-state handlers are final (non-negotiable)
+func (h *Handlers) FooBar(e *am.Event) {}
+func (h *Handlers) BarFoo(e *am.Event) {}
+func (h *Handlers) BarAny(e *am.Event) {}
+func (h *Handlers) AnyFoo(e *am.Event) {}
+```
+
+## Debugging a machine
+
+```bash
+$ am-dbg
+```
+
+```go
+import "github.com/pancsta/asyncmachine-go/pkg/telemetry"
+// ...
+err := telemetry.TransitionsToDBG(mach, "")
+```
+
+```bash
+# use AM_DEBUG to increase timeouts of NewCommon machines
+AM_DEBUG=1
+```
+
+## Simple logging
+
+```go
+mach.SetLogLevel(am.LogChanges)
+```
+
+## Custom logging
+
+```go
+// max the log level
+mach.SetLogLevel(am.LogEverything)
+// level based dispatcher
+mach.SetLogger(func(level LogLevel, msg string, args ...any) {
+ if level > am.LogChanges {
+ customLogDetails(msg, args...)
+ return
+ }
+ customLog(msg, args...)
+
+})
+// include some args in the log and traces
+mach.SetLogArgs(am.NewArgsMapper([]string{"id", "name"}, 20))
+```
+
+## Minimal machine init
+
+```go
+import am "github.com/pancsta/asyncmachine-go/pkg/machine"
+// ...
+ctx := context.Background()
+states := am.Struct{"Foo":{}, "Bar":{}}
+mach := am.New(ctx, states, &am.Opts{
+ ID: "foo1",
+})
+```
+
+## Common machine init
+
+```go
+import (
+ am "github.com/pancsta/asyncmachine-go/pkg/machine"
+ // task gen-states Foo,Bar
+ ss "repo/project/states"
+)
+// ...
+ctx := context.Background()
+mach, err := am.NewCommon(ctx, "foo1", ss.States, ss.Names, nil, nil, &am.Opts{
+ LogLevel: am.LogChanges,
+})
+```
+
+## Wait for a state activation
+
+```go
+// create a wait channel
+when := mach.When1("FileDownloaded", nil)
+// change the state
+mach.Add1("DownloadingFile", nil)
+// wait with err and timeout
+select {
+case <-time.After(5 * time.Second):
+ return am.ErrTimeout
+case <-mach.WhenErr(nil):
+ return mach.Err
+case <-when:
+ // FileDownloaded active
+}
+```
+
+## Wait for a state activation with argument values
+
+See also:
+
+- [multi states](#input-multi-state)
+
+```go
+// define args
+args := am.A{"id": 123}
+// crate a wait channel
+when := mach.WhenArgs("EventConnected", args, nil)
+// wait with err and timeout
+select {
+ case <-time.After(5 * time.Second):
+return am.ErrTimeout
+ case <-mach.WhenErr(nil):
+return mach.Err
+ case <-when:
+ // EventConnected activated with (id=1232)
+}
+```
+
+## Wait for a state de-activation
+
+```go
+// crate a wait channel
+when := mach.WhenNot1("DownloadingFile", args, nil)
+// wait with err and timeout
+select {
+case <-time.After(5 * time.Second):
+ return am.ErrTimeout
+case <-mach.WhenErr(nil):
+ return mach.Err
+case <-when:
+ // DownloadingFile inactive
+}
+```
+
+## Wait for a specific machine time
+
+```go
+// crate a wait channel
+when := mach.WhenTime(am.S{"DownloadingFile"}, am.T{6}, nil)
+// wait with err and timeout
+select {
+case <-time.After(5 * time.Second):
+ return am.ErrTimeout
+case <-mach.WhenErr(nil):
+ return mach.Err
+case <-when:
+ // DownloadingFile has tick >= 6
+}
+```
+
+## Wait for a relative state tick
+
+```go
+// crate a wait channel
+when := mach.WhenTick("DownloadingFile", 2, nil)
+// wait with err and timeout
+select {
+case <-time.After(5 * time.Second):
+ return am.ErrTimeout
+case <-mach.WhenErr(nil):
+ return mach.Err
+case <-when:
+ // DownloadingFile tick has increased by 2
+}
+```
+
+## Wait for a specific state tick
+
+```go
+// crate a wait channel
+when := mach.WhenTickEq("DownloadingFile", 6, nil)
+// wait with err and timeout
+select {
+case <-time.After(5 * time.Second):
+ return am.ErrTimeout
+case <-mach.WhenErr(nil):
+ return mach.Err
+case <-when:
+ // DownloadingFile has tick >= 6
+}
+```
+
+## Synchronous state (single)
+
+```go
+Ready: {},
+```
+
+## Asynchronous state (double)
+
+```go
+DownloadingFile: {
+ Remove: groupFileDownloaded,
+},
+FileDownloaded: {
+ Remove: groupFileDownloaded,
+},
+```
+
+## Asynchronous boolean state (triple)
+
+```go
+Connected: {
+ Remove: groupConnected,
+},
+Connecting: {
+ Remove: groupConnected,
+},
+Disconnecting: {
+ Remove: groupConnected,
+},
+```
+
+## Full asynchronous boolean state (quadruple)
+
+```go
+Connected: {
+ Remove: groupConnected,
+},
+Connecting: {
+ Remove: groupConnected,
+},
+Disconnecting: {
+ Remove: groupConnected,
+},
+Disconnected: {
+ Auto: 1,
+ Remove: groupConnected,
+},
+```
+
+## Input `Multi` state
+
+See also:
+
+- [batching input data](#batch-data-into-a-single-transition)
+- [waiting on multi states](#wait-for-a-state-activation-with-argument-values)
+
+```go
+var States = am.Struct{
+ ConnectEvent: {Multi: true},
+}
+
+func (h *Handlers) ConnectEventState(e *am.Event) {
+ // called even if the state is already active
+}
+```
+
+## Self removal state
+
+```go
+func (d *Debugger) FwdStepState(_ *am.Event) {
+ // removes itself AFTER the end of the handler
+ // like defer, but with a queue
+ d.Mach.Remove1("FwdStep", nil)
+ // ... handler
+}
+```
+
+## State context
+
+```go
+func (h *Handlers) DownloadingFileState(e *am.Event) {
+ // open until the state remains active
+ ctx := e.Machine.NewStateCtx("DownloadingFile")
+ // fork to unblock
+ go func() {
+ // check if still valid
+ if ctx.Err() != nil {
+ return // expired
+ }
+ }()
+}
+```
+
+## Step context
+
+```go
+// create a ctx just for this select statement (step)
+ctx, cancel = context.WithCancel(context.Background())
+select {
+ case <-mach.WhenErr(ctx):
+ cancel()
+ return mach.Err
+ case <-time.After(5 * time.Second):
+ cancel()
+ return am.ErrTimeout
+ case <-mach.When1("Foo", ctx):
+ case <-mach.WhenArgs("Bar", am.A{"id": 1}, ctx):
+}
+// dispose "when" listeners as soon as they aren't needed
+cancel()
+```
+
+## Event handlers
+
+```go
+events := []string{"FileDownloaded", am.EventQueueEnd}
+ch := mach.OnEvent(events, nil)
+for e, ok := <-ch; ok; {
+ if e.Name == EventQueueEnd {
+ continue
+ }
+}
+```
+
+## Channel responses via arguments
+
+```go
+// buffered channels
+req := &myReq{
+ resp: make(chan []string, 1),
+ err: make(chan error, 1),
+}
+// sync push to the handler
+mach.Add1("GetPeers", am.A{"myReq": req})
+// await resp and err
+select {
+case resp := <-req.resp:
+ return resp, nil
+case err := <-req.err:
+ return nil, err
+case <-mach.Ctx.Done():
+ return nil, mach.Ctx.Err()
+```
+
+```go
+func (p *PubSub) GetPeersState(e *am.Event) {
+ p.Mach.Remove1(ss.GetPeers, nil)
+
+ req := e.Args["myReq"].(*myReq)
+
+ // ...
+
+ // buffered
+ req.resp <- out
+}
+```
+
+## Equivalent of `select` write with `default`
+
+```go
+// vanilla
+select {
+case p.newPeers <- struct{}{}:
+default:
+ // canceled
+}
+
+// asyncmachine
+res := p.mach.Add1("PeersPending", nil)
+if res == am.Canceled {
+ // canceled
+}
+```
+
+## Custom exception handler
+
+```go
+type Handlers struct {
+ *am.ExceptionHandler
+}
+
+func (h *Handlers) ExceptionState(e *am.Event) {
+ // custom handling logic
+ // ...
+
+ // call the parent error handler (or not)
+ h.ExceptionHandler.ExceptionState(e)
+}
+```
+
+## Verify states at runtime
+
+See also:
+
+- [typesafe states pkg](#typesafe-states-pkg-template)
+
+```go
+package sim
+
+import (
+ ss "URL/states/NAME"
+ am "github.com/pancsta/asyncmachine-go/pkg/machine"
+)
+
+func main() {
+ m := am.New(nil, ss.States, nil)
+ err := m.VerifyStates(ss.Names)
+ if err != nil {
+ print(err)
+ }
+ print(m.Inspect(nil))
+}
+```
+
+## Typesafe states pkg template
+
+```go
+// generate using either
+// $ am-gen states-file Foo,Bar
+// $ task gen-states-file Foo,Bar
+package states
+
+import am "github.com/pancsta/asyncmachine-go/pkg/machine"
+
+// S is a type alias for a list of state names.
+type S = am.S
+
+// States map defines relations and properties of states.
+var States = am.Struct{
+ Start: {},
+ Heartbeat: {Require: S{Start}},
+}
+
+//#region boilerplate defs
+
+// Names of all the states (pkg enum).
+
+const (
+ Start = "Start"
+ Heartbeat = "Heartbeat"
+)
+
+// Names is an ordered list of all the state names.
+var Names = S{Start, Heartbeat}
+
+//#endregion
+
+```
+
+## State Mutex Groups
+
+```go
+var States = am.Struct{
+ Connected: {Remove: groupConnected},
+ Connecting: {Remove: groupConnected},
+ Disconnecting: {Remove: groupConnected},
+}
+
+// Only 1 of the group can be active at a given time.
+var groupConnected = am.S{Connecting, Connected, Disconnecting}
+```
+
+## Transition-aware handler
+
+```go
+func (h *Handlers) ClientSelectedEnd(e *am.Event) {
+ // clean up, except when switching to SelectingClient
+ if e.Mutation().CalledStatesHas(ss.SelectingClient) {
+ return
+ }
+ h.cleanUp()
+}
+```
+
+## Batch data into a single transition
+
+```go
+var queue []*Msg
+var queueMx sync.Mutex
+var scheduled bool
+
+func Msg(msgTx *Msg) {
+ queueMx.Lock()
+ defer queueMx.Unlock()
+
+ if !scheduled {
+ scheduled = true
+ go func() {
+ // wait some time
+ time.Sleep(time.Second)
+
+ queueMx.Lock()
+ defer queueMx.Unlock()
+
+ // add in bulk
+ mach.Add1("Msgs", am.A{"msgs": queue})
+ queue = nil
+ scheduled = false
+ }()
+ }
+ // enqueue
+ queue = append(queue, msgTx)
+}
+
+```
+
+### Switch a state group
+
+```go
+switch mach.Switch(ss.GroupPlaying...) {
+case ss.Playing:
+case ss.TailMode:
+default:
+}
+```
+
+### DiffStates to navigate the flow
+
+```go
+func (d *Debugger) HelpDialogEnd(e *am.Event) {
+ diff := am.DiffStates(ss.GroupDialog, e.Transition().TargetStates)
+ if len(diff) == len(ss.GroupDialog) {
+ // all dialogs closed, show main
+ d.layoutRoot.SendToFront("main")
+ }
+}
+```
+
+### Pass data from a negotiation handler to the final handler
+
+```go
+
+func (s *Sim) MsgRandomTopicEnter(e *am.Event) bool {
+
+ p := s.pickRandPeer()
+ if p == nil || len(p.simTopics) == 0 {
+ // not enough topics
+ return false
+ }
+ randTopic := p.simTopics[rand.Intn(len(p.simTopics))]
+
+ if len(s.GetTopicPeers(randTopic)) < 2 {
+ // Not enough peers in topic
+ return false
+ }
+
+ // pass the topic
+ e.Args["Topic.id"] = randTopic
+
+ return len(s.topics) > 0
+}
+```
+
+### Check if a group od states is active
+
+```go
+if d.Mach.Any1(ss.GroupDialog...) {
+ d.Mach.Remove(ss.GroupDialog, nil)
+ return nil
+}
+```
diff --git a/docs/manual.md b/docs/manual.md
index 2b01ad6..5ac5a6d 100644
--- a/docs/manual.md
+++ b/docs/manual.md
@@ -1,5 +1,8 @@
# asyncmachine-go manual
+This manual hasn't been updated to `v0.4.0`. Please refer to [Cookbook](/docs/cookbook.md) and [/examples](/examples)
+for now.
+
- [asyncmachine-go manual](#asyncmachine-go-manual)
- [States](#states)
- [Everything is a state](#everything-is-a-state)
diff --git a/examples/fsm/fsm_test.go b/examples/fsm/fsm_test.go
new file mode 100644
index 0000000..d2a1a9b
--- /dev/null
+++ b/examples/fsm/fsm_test.go
@@ -0,0 +1,118 @@
+// Based on https://en.wikipedia.org/wiki/Finite-state_machine
+//
+//=== RUN TestTurnstile
+//[state] +Locked
+// fsm_test.go:74: Push into (Locked:1)
+// fsm_test.go:79: Coin into (Locked:1)
+//[state] +InputCoin
+//[state] -InputCoin
+//[state] +Unlocked -Locked
+// fsm_test.go:85: Coin into (Unlocked:1)
+//[state] +InputCoin
+//[state] -InputCoin
+// fsm_test.go:91: Push into (Unlocked:1)
+//[state] +InputPush
+//[state] -InputPush
+//[state] +Locked -Unlocked
+// fsm_test.go:96: End with (Locked:3)
+//--- PASS: TestTurnstile (0.00s)
+//PASS
+
+package fsm
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ am "github.com/pancsta/asyncmachine-go/pkg/machine"
+
+ "github.com/stretchr/testify/assert"
+)
+
+// state enum pkg
+
+var (
+ states = am.Struct{
+ // input states
+ InputPush: {},
+ InputCoin: {},
+
+ // "state" states
+ Locked: {
+ Auto: true,
+ Remove: groupUnlocked,
+ },
+ Unlocked: {Remove: groupUnlocked},
+ }
+
+ groupUnlocked = am.S{Locked, Unlocked}
+
+ InputPush = "InputPush"
+ InputCoin = "InputCoin"
+ Locked = "Locked"
+ Unlocked = "Unlocked"
+
+ Names = am.S{InputPush, InputCoin, Locked, Unlocked}
+)
+
+// handlers
+
+type Turnstile struct{}
+
+func (t *Turnstile) InputPushEnter(e *am.Event) bool {
+ return e.Machine.Is1(Unlocked)
+}
+
+func (t *Turnstile) InputPushState(e *am.Event) {
+ e.Machine.Remove1(InputPush, nil)
+ e.Machine.Add1(Locked, nil)
+}
+
+func (t *Turnstile) InputCoinState(e *am.Event) {
+ e.Machine.Remove1(InputCoin, nil)
+ e.Machine.Add1(Unlocked, nil)
+}
+
+// example
+
+func TestTurnstile(t *testing.T) {
+ mach := am.New(context.Background(), states, &am.Opts{
+ ID: "turnstile",
+ DontPanicToException: true,
+ DontLogID: true,
+ LogLevel: am.LogChanges,
+ HandlerTimeout: time.Minute,
+ })
+
+ _ = mach.BindHandlers(&Turnstile{})
+ _ = mach.VerifyStates(Names)
+
+ // start
+ mach.Add1(Locked, nil)
+ assert.True(t, mach.Is1(Locked))
+
+ t.Logf("Push into %s", mach)
+ mach.Add1(InputPush, nil)
+ assert.True(t, mach.Is1(Locked))
+
+ // coin
+ t.Logf("Coin into %s", mach)
+ mach.Add1(InputCoin, nil)
+ assert.True(t, mach.Not1(Locked))
+ assert.True(t, mach.Is1(Unlocked))
+
+ // coin
+ t.Logf("Coin into %s", mach)
+ mach.Add1(InputCoin, nil)
+ assert.True(t, mach.Not1(Locked))
+ assert.True(t, mach.Is1(Unlocked))
+
+ // push
+ t.Logf("Push into %s", mach)
+ mach.Add1(InputPush, nil)
+ assert.True(t, mach.Not1(Unlocked))
+ assert.True(t, mach.Is1(Locked))
+
+ t.Logf("End with %s", mach)
+}
diff --git a/examples/nfa/nfa_test.go b/examples/nfa/nfa_test.go
new file mode 100644
index 0000000..c7c6648
--- /dev/null
+++ b/examples/nfa/nfa_test.go
@@ -0,0 +1,217 @@
+// Based on https://en.wikipedia.org/wiki/Nondeterministic_finite_automaton
+//
+//=== RUN TestRegexp
+//=== RUN TestRegexp/test_101010_(OK)
+//[state] +Start +StepX
+//[state] +Input
+//[extern:InputState] input: 1
+//[state] +Input
+//[extern:InputState] input: 0
+//[state] +Input
+//[extern:InputState] input: 1
+//[state] +Step0 -StepX
+//[state] +Input
+//[extern:InputState] input: 0
+//[state] +Step1 -Step0
+//[state] +Input
+//[extern:InputState] input: 1
+//[state] +Step2 -Step1
+//[state] +Input
+//[extern:InputState] input: 0
+//[state] +Step3 -Step2
+//[state] -Start
+//--- PASS: TestRegexp (0.00s)
+// --- PASS: TestRegexp/test_101010_(OK) (0.00s)
+//PASS
+
+package nfa
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ am "github.com/pancsta/asyncmachine-go/pkg/machine"
+)
+
+// state enum pkg
+
+var (
+ states = am.Struct{
+ // input states
+ Input: {Multi: true},
+
+ // action states
+ Start: {Add: am.S{StepX}},
+
+ // "state" states
+ StepX: {Remove: groupSteps},
+ Step0: {Remove: groupSteps},
+ Step1: {Remove: groupSteps},
+ Step2: {Remove: groupSteps},
+ Step3: {Remove: groupSteps},
+ }
+
+ groupSteps = am.S{StepX, Step0, Step1, Step2, Step3}
+
+ Start = "Start"
+ Input = "Input"
+ StepX = "StepX"
+ Step0 = "Step0"
+ Step1 = "Step1"
+ Step2 = "Step2"
+ Step3 = "Step3"
+
+ Names = am.S{Start, Input, StepX, Step0, Step1, Step2, Step3, am.Exception}
+)
+
+// handlers
+
+type Regexp struct {
+ input string
+ cursor int
+}
+
+func (t *Regexp) StartState(e *am.Event) {
+ mach := e.Machine
+
+ // reset
+ t.input = e.Args["input"].(string)
+ t.cursor = 0
+ mach.Add1(StepX, nil)
+
+ // jump out of the queue
+ go func() {
+ // TODO use mach.WhenQueueEnds()
+ <-mach.When1(StepX, nil)
+ // needed for the queue lock to release
+ time.Sleep(1 * time.Millisecond)
+
+ for _, c := range t.input {
+ switch c {
+
+ case '0':
+ mach.Add1(Input, am.A{"input": "0"})
+
+ case '1':
+ mach.Add1(Input, am.A{"input": "1"})
+ }
+
+ t.cursor++
+ }
+
+ mach.Remove1(Start, nil)
+ }()
+}
+
+func (t *Regexp) InputState(e *am.Event) {
+ mach := e.Machine
+ input := e.Args["input"].(string)
+ mach.Log("input: %s", input)
+
+ if input == "0" {
+ t.input0(mach)
+ } else {
+ t.input1(mach)
+ }
+}
+
+func (t *Regexp) input0(mach *am.Machine) {
+ switch mach.Switch(groupSteps...) {
+ case StepX:
+ mach.Add1(StepX, nil)
+
+ case Step0:
+ mach.Add1(Step1, nil)
+
+ case Step1:
+ mach.Add1(Step2, nil)
+
+ case Step2:
+ mach.Add1(Step3, nil)
+ }
+}
+
+func (t *Regexp) input1(mach *am.Machine) {
+ switch mach.Switch(groupSteps...) {
+ case StepX:
+ // TODO should use CanAdd and State0Enter
+ if t.cursor+3 == len(t.input)-1 {
+ mach.Add1(Step0, nil)
+ } else {
+ mach.Add1(StepX, nil)
+ }
+
+ case Step0:
+ mach.Add1(Step1, nil)
+
+ case Step1:
+ mach.Add1(Step2, nil)
+
+ case Step2:
+ mach.Add1(Step3, nil)
+ }
+}
+
+// example
+
+func TestRegexp(t *testing.T) {
+ var err error
+ mach := am.New(context.Background(), states, &am.Opts{
+ ID: "regexp",
+ DontPanicToException: true,
+ DontLogID: true,
+ LogLevel: am.LogChanges,
+ // LogLevel: am.LogOps,
+ // LogLevel: am.LogDecisions,
+ HandlerTimeout: time.Minute,
+ })
+
+ _ = mach.BindHandlers(&Regexp{})
+ err = mach.VerifyStates(Names)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // tests
+ tests := []struct {
+ name string
+ input string
+ expect bool
+ }{
+ {
+ name: "test 101010 (OK)",
+ input: "101010",
+ expect: true,
+ },
+ {
+ name: "test 1010 (OK)",
+ input: "1010",
+ expect: true,
+ },
+ {
+ name: "test 100010 (NOT OK)",
+ input: "100010",
+ expect: false,
+ },
+ }
+
+ // code
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ mach.Add1(Start, am.A{
+ "input": tt.input,
+ })
+
+ // wait for Start to finish
+ <-mach.WhenNot1(Start, nil)
+
+ // assert
+ if tt.expect && mach.Not1(Step3) {
+ t.Fatal("Expected Step3")
+ } else if !tt.expect && mach.Is1(Step3) {
+ t.Fatal("Didn't expect Step3")
+ }
+ })
+ }
+}
diff --git a/examples/temporal-expense/expense_test.go b/examples/temporal-expense/expense_test.go
index ee664a5..0806ee6 100644
--- a/examples/temporal-expense/expense_test.go
+++ b/examples/temporal-expense/expense_test.go
@@ -1,7 +1,5 @@
// Based on https://github.com/temporalio/samples-go/blob/main/expense/
//
-// Go playgroundL https://play.golang.com/p/P1eg6tKh6E4
-//
// This example shows a simple payment workflow with an async approval input,
// going from CreatingExpense to PaymentCompleted.
//
@@ -9,68 +7,64 @@
//
// Sample output (LogLevel == LogChanges):
//
-// === RUN TestExpense
-// expense_test.go:160: [7dce4] [state] +CreatingExpense
-// expense_test.go:180: waiting: CreatingExpense to WaitingForApproval
-// expense_test.go:266: expense API called
-// expense_test.go:244: approval request received: 123
-// expense_test.go:160: [7dce4] [state] +ExpenseCreated -CreatingExpense
-// expense_test.go:160: [7dce4] [state:auto] +WaitingForApproval
-// expense_test.go:196: waiting: WaitingForApproval to ApprovalGranted
-// expense_test.go:247: granting fake approval
-// expense_test.go:249: sent fake approval
-// expense_test.go:204: received approval ID: fake
-// expense_test.go:196: waiting: WaitingForApproval to ApprovalGranted
-// expense_test.go:252: granting real approval
-// expense_test.go:254: sent real approval
-// expense_test.go:204: received approval ID: 123
-// expense_test.go:160: [7dce4] [state] +ApprovalGranted -WaitingForApproval
-// expense_test.go:160: [7dce4] [state:auto] +PaymentInProgress
-// expense_test.go:196: waiting: WaitingForApproval to ApprovalGranted
-// expense_test.go:224: waiting: PaymentInProgress to PaymentCompleted
-// expense_test.go:279: payment API called
-// expense_test.go:160: [7dce4] [state] +PaymentCompleted -PaymentInProgress
-// expense_test.go:292:
-// (PaymentCompleted:1 ApprovalGranted:1 ExpenseCreated:1)
-// expense_test.go:293:
-// (ExpenseCreated:1 ApprovalGranted:1 PaymentCompleted:1)[CreatingExpense:1 WaitingForApproval:1 PaymentInProgress:1 Exception:0]
-// expense_test.go:294:
-// CreatingExpense:
-// State: false 1
-// Remove: ExpenseCreated
+//=== RUN TestExpense
+// expense_test.go:145: [state] +CreatingExpense
+// expense_test.go:145: [state:auto] +WaitingForApproval +PaymentInProgress
+// expense_test.go:164: waiting: CreatingExpense to WaitingForApproval
+// expense_test.go:180: waiting: WaitingForApproval to ApprovalGranted
+// expense_test.go:250: expense API called
+// expense_test.go:233: approval request received: 123
+// expense_test.go:263: payment API called
+// expense_test.go:145: [state] +ExpenseCreated -CreatingExpense
+// expense_test.go:145: [state] +PaymentCompleted -PaymentInProgress
+// expense_test.go:237: granting fake approval
+// expense_test.go:239: sent fake approval
+// expense_test.go:192: received approval ID: fake
+// expense_test.go:180: waiting: WaitingForApproval to ApprovalGranted
+// expense_test.go:243: granting real approval
+// expense_test.go:192: received approval ID: 123
+// expense_test.go:245: sent real approval
+// expense_test.go:145: [state] +ApprovalGranted -WaitingForApproval
+// expense_test.go:180: waiting: WaitingForApproval to ApprovalGranted
+// expense_test.go:213: waiting: PaymentInProgress to PaymentCompleted
+// expense_test.go:280:
+// (ApprovalGranted:1 PaymentCompleted:1 ExpenseCreated:1)
+// expense_test.go:281:
+// (ApprovalGranted:1 ExpenseCreated:1 PaymentCompleted:1)[CreatingExpense:2 Exception:0 PaymentInProgress:2 WaitingForApproval:2]
+// expense_test.go:282:
+// ApprovalGranted:
+// State: true 1
+// Remove: WaitingForApproval
//
-// ExpenseCreated:
-// State: true 1
-// Remove: CreatingExpense
+// CreatingExpense:
+// State: false 2
+// Remove: ExpenseCreated
//
-// WaitingForApproval:
-// State: false 1
-// Auto: true
-// Require: ExpenseCreated
-// Remove: ApprovalGranted
+// Exception:
+// State: false 0
+// Multi: true
//
-// ApprovalGranted:
-// State: true 1
-// Require: ExpenseCreated
-// Remove: WaitingForApproval
+// ExpenseCreated:
+// State: true 1
+// Remove: CreatingExpense
//
-// PaymentInProgress:
-// State: false 1
-// Auto: true
-// Require: ApprovalGranted
-// Remove: PaymentCompleted
+// PaymentCompleted:
+// State: true 1
+// Remove: PaymentInProgress
//
-// PaymentCompleted:
-// State: true 1
-// Require: ExpenseCreated ApprovalGranted
-// Remove: PaymentInProgress
+// PaymentInProgress:
+// State: false 2
+// Auto: true
+// Remove: PaymentCompleted
//
-// Exception:
-// State: false 0
+// WaitingForApproval:
+// State: false 2
+// Auto: true
+// Remove: ApprovalGranted
//
//
-// --- PASS: TestExpense (2.00s)
-// PASS
+//--- PASS: TestExpense (0.02s)
+//PASS
package main
@@ -84,7 +78,9 @@ import (
"testing"
"time"
+ sse "github.com/pancsta/asyncmachine-go/examples/temporal-expense/states"
am "github.com/pancsta/asyncmachine-go/pkg/machine"
+ //"github.com/pancsta/asyncmachine-go/pkg/telemetry"
)
var (
@@ -98,35 +94,9 @@ type Logger func(msg string, args ...any)
// PaymentCompleted flow.
func NewMachine(ctx context.Context) *am.Machine {
// define states
- return am.New(ctx, am.States{
- // CreateExpenseActivity
- "CreatingExpense": {
- Remove: am.S{"ExpenseCreated"},
- },
- "ExpenseCreated": {
- Remove: am.S{"CreatingExpense"},
- },
- // WaitForDecisionActivity
- "WaitingForApproval": {
- Auto: true,
- Require: am.S{"ExpenseCreated"},
- Remove: am.S{"ApprovalGranted"},
- },
- "ApprovalGranted": {
- Require: am.S{"ExpenseCreated"},
- Remove: am.S{"WaitingForApproval"},
- },
- // PaymentActivity
- "PaymentInProgress": {
- Auto: true,
- Require: am.S{"ApprovalGranted"},
- Remove: am.S{"PaymentCompleted"},
- },
- "PaymentCompleted": {
- Require: am.S{"ExpenseCreated", "ApprovalGranted"},
- Remove: am.S{"PaymentInProgress"},
- },
- }, nil)
+ return am.New(ctx, sse.States, &am.Opts{
+ DontLogID: true,
+ })
}
// MachineHandlers is a struct of handlers & their data for the Expense machine.
@@ -143,15 +113,16 @@ func (h *MachineHandlers) CreatingExpenseState(e *am.Event) {
// args definition
h.expenseID = e.Args["expenseID"].(string)
// get a context of this particular state's instance (clock's tick)
- stateCtx := e.Machine.GetStateCtx("CreatingExpense")
+ stateCtx := e.Machine.NewStateCtx(sse.CreatingExpense)
- // never block in a handler, always "go func" it
+ // never block in a handler, always fork
go func() {
resp, err := http.Get(expenseBackendURL + "?id=" + h.expenseID)
if err != nil {
e.Machine.AddErr(err)
return
}
+
body, err := io.ReadAll(resp.Body)
_ = resp.Body.Close()
if err != nil {
@@ -162,11 +133,12 @@ func (h *MachineHandlers) CreatingExpenseState(e *am.Event) {
e.Machine.AddErrStr(string(body))
return
}
+
// make sure this should still be happening
if stateCtx.Err() != nil {
- return
+ return // expired
}
- e.Machine.Add(am.S{"ExpenseCreated"}, nil)
+ e.Machine.Add1(sse.ExpenseCreated, nil)
}()
}
@@ -204,7 +176,7 @@ func (h *MachineHandlers) PaymentInProgressState(e *am.Event) {
return
}
- e.Machine.Add(am.S{"PaymentCompleted"}, nil)
+ e.Machine.Add1(sse.PaymentCompleted, nil)
}()
}
@@ -239,8 +211,8 @@ func ExpenseFlow(
// reusable error channel
errCh := machine.WhenErr(nil)
- // start it up!
- machine.Add(am.S{"CreatingExpense"}, am.A{"expenseID": expenseID})
+ // start the flow
+ machine.Add1(sse.CreatingExpense, am.A{"expenseID": expenseID})
// CreatingExpense is an automatic state, it will add itself at this point.
// string(machine) == [Enabled, CreatingExpense]
@@ -252,7 +224,7 @@ func ExpenseFlow(
return machine, errors.New("timeout")
case <-errCh:
return machine, machine.Err
- case <-machine.When(am.S{"WaitingForApproval"}, nil):
+ case <-machine.When1(sse.WaitingForApproval, nil):
// WaitingForApproval is an automatic state
}
@@ -263,24 +235,30 @@ func ExpenseFlow(
granted := false
for !granted {
log("waiting: WaitingForApproval to ApprovalGranted")
+
// wait with a timeout
select {
+
// new approvals
case approvedID, ok := <-approvalCh:
if !ok {
return machine, errors.New("approval channel closed")
}
- log("received approval ID: %s", approvedID)
+
// TRY to approve the existing expense
- machine.Add(am.S{"ApprovalGranted"}, am.A{"approvedID": approvedID})
+ log("received approval ID: %s", approvedID)
+ machine.Add1(sse.ApprovalGranted, am.A{"approvedID": approvedID})
+
// approval timeout
case <-time.After(10 * time.Minute):
return machine, errors.New("timeout")
+
// error or machine disposed
case <-errCh:
return machine, machine.Err
+
// approval granted
- case <-machine.When(am.S{"ApprovalGranted"}, nil):
+ case <-machine.When1(sse.ApprovalGranted, nil):
granted = true
}
}
@@ -293,7 +271,7 @@ func ExpenseFlow(
select {
case <-errCh:
return machine, machine.Err
- case <-machine.When(am.S{"PaymentCompleted"}, nil):
+ case <-machine.When1(sse.PaymentCompleted, nil):
}
_ = machine.String() // (ExpenseCreated:1 ApprovalGranted:1 PaymentCompleted:1)
@@ -311,24 +289,19 @@ func TestExpense(t *testing.T) {
expenseId := <-expenseCh
t.Log("approval request received: " + expenseId)
time.Sleep(10 * time.Millisecond)
+
// approve a random ID
t.Log("granting fake approval")
approvalCh <- "fake"
t.Log("sent fake approval")
time.Sleep(10 * time.Millisecond)
+
// approve our ID
t.Log("granting real approval")
approvalCh <- expenseId
t.Log("sent real approval")
}()
- // mock an external approval flow
- // go func() {
- // println("TEST3")
- // println(<-approvalCh)
- // println("TEST4")
- // }()
-
// mock the expense API
expenseAPI := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Log("expense API called")
@@ -346,7 +319,7 @@ func TestExpense(t *testing.T) {
) {
t.Log("payment API called")
// payment successful
- fmt.Fprint(w, "SUCCEED")
+ _, _ = fmt.Fprint(w, "SUCCEED")
}))
defer paymentAPI.Close()
paymentBackendURL = paymentAPI.URL
@@ -356,7 +329,7 @@ func TestExpense(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- if !machine.Is(am.S{"PaymentCompleted"}) {
+ if !machine.Is1(sse.PaymentCompleted) {
t.Fatal("not PaymentCompleted")
}
diff --git a/examples/temporal-expense/states/ss_expense.go b/examples/temporal-expense/states/ss_expense.go
new file mode 100644
index 0000000..5e80c86
--- /dev/null
+++ b/examples/temporal-expense/states/ss_expense.go
@@ -0,0 +1,48 @@
+package states
+
+import am "github.com/pancsta/asyncmachine-go/pkg/machine"
+
+// S is a type alias for a list of state names.
+type S = am.S
+
+// States map defines relations and properties of states.
+var States = am.Struct{
+ CreatingExpense: {Remove: GroupExpense},
+ ExpenseCreated: {Remove: GroupExpense},
+ WaitingForApproval: {
+ Auto: true,
+ Remove: GroupApproval,
+ },
+ ApprovalGranted: {Remove: GroupApproval},
+ PaymentInProgress: {
+ Auto: true,
+ Remove: GroupPayment,
+ },
+ PaymentCompleted: {Remove: GroupPayment},
+}
+
+// Groups of mutually exclusive states.
+
+var (
+ GroupExpense = S{CreatingExpense, ExpenseCreated}
+ GroupApproval = S{WaitingForApproval, ApprovalGranted}
+ GroupPayment = S{PaymentInProgress, PaymentCompleted}
+)
+
+//#region boilerplate defs
+
+// Names of all the states (pkg enum).
+
+const (
+ CreatingExpense = "CreatingExpense"
+ ExpenseCreated = "ExpenseCreated"
+ WaitingForApproval = "WaitingForApproval"
+ ApprovalGranted = "ApprovalGranted"
+ PaymentInProgress = "PaymentInProgress"
+ PaymentCompleted = "PaymentCompleted"
+)
+
+// Names is an ordered list of all the state names.
+var Names = S{CreatingExpense, ExpenseCreated, WaitingForApproval, ApprovalGranted, PaymentInProgress, PaymentCompleted}
+
+//#endregion
diff --git a/examples/temporal-fileprocessing/fileprocessing.go b/examples/temporal-fileprocessing/fileprocessing.go
index a593333..08ad4ad 100644
--- a/examples/temporal-fileprocessing/fileprocessing.go
+++ b/examples/temporal-fileprocessing/fileprocessing.go
@@ -1,65 +1,64 @@
// Based on https://github.com/temporalio/samples-go/blob/main/fileprocessing/
//
-// Go playground: https://play.golang.com/p/Fv92Xpzlzv6
-//
// This example shows a simple file processing workflow going from
// DownloadingFile to FileUploaded.
//
// Sample output (LogLevel == LogChanges):
//
-// === RUN TestFileProcessing
-// fileprocessing.go:164: [05a4d] [state] +DownloadingFile
-// fileprocessing.go:164: [05a4d] [external] Downloading file... foo.txt
-// fileprocessing.go:178: waiting: DownloadingFile to FileUploaded
-// fileprocessing.go:164: [05a4d] [state] +FileDownloaded -DownloadingFile
-// fileprocessing.go:164: [05a4d] [state:auto] +ProcessingFile
-// fileprocessing.go:164: [05a4d] [external] processFileActivity succeed /tmp/temporal_sample1737998348
-// fileprocessing.go:164: [05a4d] [state] +FileProcessed -ProcessingFile
-// fileprocessing.go:164: [05a4d] [external] cleanup /tmp/temporal_sample3389363275
-// fileprocessing.go:164: [05a4d] [state:auto] +UploadingFile
-// fileprocessing.go:164: [05a4d] [external] uploadFileActivity begin /tmp/temporal_sample1737998348
-// fileprocessing.go:164: [05a4d] [external] uploadFileActivity succeed /tmp/temporal_sample1737998348
-// fileprocessing.go:164: [05a4d] [state] +FileUploaded -UploadingFile
-// fileprocessing.go:164: [05a4d] [external] cleanup /tmp/temporal_sample1737998348
-// fileprocessing_test.go:19:
-// (FileUploaded:1 FileProcessed:1 FileDownloaded:1)
-// fileprocessing_test.go:20:
-// (FileDownloaded:1 FileProcessed:1 FileUploaded:1)[Exception:0 DownloadingFile:1 ProcessingFile:1 UploadingFile:1]
-// fileprocessing_test.go:21:
-// Exception:
-// State: false 0
+//=== RUN TestFileProcessing
+// fileprocessing.go:156: [dad73] [state] +DownloadingFile
+// fileprocessing.go:156: [dad73] [extern:DownloadingF...] Downloading file... foo.txt
+// fileprocessing.go:169: waiting: DownloadingFile to FileUploaded
+// fileprocessing.go:156: [dad73] [state] +FileDownloaded -DownloadingFile
+// fileprocessing.go:156: [dad73] [state:auto] +ProcessingFile
+// fileprocessing.go:156: [dad73] [extern] processFileActivity succeed /tmp/temporal_sample920978545
+// fileprocessing.go:156: [dad73] [state] +FileProcessed -ProcessingFile
+// fileprocessing.go:156: [dad73] [extern:ProcessingFi...] cleanup /tmp/temporal_sample699261295
+// fileprocessing.go:156: [dad73] [state:auto] +UploadingFile
+// fileprocessing.go:156: [dad73] [extern] uploadFileActivity begin /tmp/temporal_sample920978545
+// fileprocessing.go:156: [dad73] [extern] uploadFileActivity succeed /tmp/temporal_sample920978545
+// fileprocessing.go:156: [dad73] [state] +FileUploaded -UploadingFile
+// fileprocessing.go:156: [dad73] [extern:UploadingFil...] cleanup /tmp/temporal_sample920978545
+// fileprocessing_test.go:34:
+// (FileUploaded:1 FileProcessed:1 FileDownloaded:1)
+// fileprocessing_test.go:35:
+// (FileDownloaded:1 FileProcessed:1 FileUploaded:1)[DownloadingFile:2 Exception:0 ProcessingFile:2 UploadingFile:2]
+// fileprocessing_test.go:36:
+// DownloadingFile:
+// State: false 2
+// Remove: FileDownloaded
//
-// DownloadingFile:
-// State: false 1
-// Remove: FileDownloaded
+// Exception:
+// State: false 0
+// Multi: true
//
-// FileDownloaded:
-// State: true 1
-// Remove: DownloadingFile
+// FileDownloaded:
+// State: true 1
+// Remove: DownloadingFile
//
-// ProcessingFile:
-// State: false 1
-// Auto: true
-// Require: FileDownloaded
-// Remove: FileProcessed
+// FileProcessed:
+// State: true 1
+// Remove: ProcessingFile
//
-// FileProcessed:
-// State: true 1
-// Remove: ProcessingFile
+// FileUploaded:
+// State: true 1
+// Remove: UploadingFile
//
-// UploadingFile:
-// State: false 1
-// Auto: true
-// Require: FileProcessed
-// Remove: FileUploaded
+// ProcessingFile:
+// State: false 2
+// Auto: true
+// Require: FileDownloaded
+// Remove: FileProcessed
//
-// FileUploaded:
-// State: true 1
-// Remove: UploadingFile
+// UploadingFile:
+// State: false 2
+// Auto: true
+// Require: FileProcessed
+// Remove: FileUploaded
//
//
-// --- PASS: TestFileProcessing (0.20s)
-// PASS
+//--- PASS: TestFileProcessing (0.20s)
+//PASS
package temporal_fileprocessing
@@ -70,7 +69,9 @@ import (
"strings"
"time"
+ ssf "github.com/pancsta/asyncmachine-go/examples/temporal-fileprocessing/states"
am "github.com/pancsta/asyncmachine-go/pkg/machine"
+ //"github.com/pancsta/asyncmachine-go/pkg/telemetry"
)
type Logger func(msg string, args ...any)
@@ -79,33 +80,9 @@ type Logger func(msg string, args ...any)
// FileUploaded flow.
func NewMachine(ctx context.Context) *am.Machine {
// define states
- return am.New(ctx, am.States{
- // DownloadFileActivity
- "DownloadingFile": {
- Remove: am.S{"FileDownloaded"},
- },
- "FileDownloaded": {
- Remove: am.S{"DownloadingFile"},
- },
- // ProcessFileActivity
- "ProcessingFile": {
- Auto: true,
- Require: am.S{"FileDownloaded"},
- Remove: am.S{"FileProcessed"},
- },
- "FileProcessed": {
- Remove: am.S{"ProcessingFile"},
- },
- // UploadFileActivity
- "UploadingFile": {
- Auto: true,
- Require: am.S{"FileProcessed"},
- Remove: am.S{"FileUploaded"},
- },
- "FileUploaded": {
- Remove: am.S{"UploadingFile"},
- },
- }, nil)
+ return am.New(ctx, ssf.States, &am.Opts{
+ DontLogID: true,
+ })
}
// MachineHandlers is a struct of handlers & their data for the FileProcessing
@@ -122,7 +99,7 @@ type MachineHandlers struct {
// DownloadingFileState is a _final_ entry handler for the DownloadingFile
// state.
func (h *MachineHandlers) DownloadingFileState(e *am.Event) {
- // args definition
+ // read args
h.Filename = e.Args["filename"].(string)
e.Machine.Log("Downloading file... %s", h.Filename)
@@ -137,7 +114,7 @@ func (h *MachineHandlers) DownloadingFileState(e *am.Event) {
}
h.DownloadedName = tmpFile.Name()
// done, next step
- e.Machine.Add(am.S{"FileDownloaded"}, nil)
+ e.Machine.Add1(ssf.FileDownloaded, nil)
}()
}
@@ -147,7 +124,7 @@ func (h *MachineHandlers) ProcessingFileState(e *am.Event) {
// Never block in a handler, always "go func" it.
// State context will confirm that processing should still be happening.
// Using machine's context directly will conflict with retry logic (if any).
- stateCtx := e.Machine.GetStateCtx("ProcessingFile")
+ stateCtx := e.Machine.NewStateCtx("ProcessingFile")
go func() {
// assert context
if stateCtx.Err() != nil {
@@ -174,7 +151,7 @@ func (h *MachineHandlers) ProcessingFileState(e *am.Event) {
h.ProcessedFileName = tmpFile.Name()
e.Machine.Log("processFileActivity succeed %s", h.ProcessedFileName)
// done, next step
- e.Machine.Add(am.S{"FileProcessed"}, nil)
+ e.Machine.Add1(ssf.FileProcessed, nil)
}()
}
@@ -192,7 +169,7 @@ func (h *MachineHandlers) UploadingFileState(e *am.Event) {
// Never block in a handler, always "go func" it.
// State context will confirm that uploading should still be happening.
// Using machine's context directly will conflict with retry logic (if any).
- stateCtx := e.Machine.GetStateCtx("ProcessingFile")
+ stateCtx := e.Machine.NewStateCtx("ProcessingFile")
go func() {
e.Machine.Log("uploadFileActivity begin %s", h.ProcessedFileName)
err := h.BlobStore.uploadFile(stateCtx, h.ProcessedFileName)
@@ -203,7 +180,7 @@ func (h *MachineHandlers) UploadingFileState(e *am.Event) {
}
e.Machine.Log("uploadFileActivity succeed %s", h.ProcessedFileName)
// done, next step
- e.Machine.Add(am.S{"FileUploaded"}, nil)
+ e.Machine.Add1(ssf.FileUploaded, nil)
}()
}
@@ -219,6 +196,11 @@ func (h *MachineHandlers) UploadingFileEnd(e *am.Event) {
func FileProcessingFlow(ctx context.Context, log Logger, filename string) (*am.Machine, error) {
// init
machine := NewMachine(ctx)
+ // debug
+ //err := telemetry.TransitionsToDBG(machine, telemetry.DbgHost)
+ //if err != nil {
+ // return nil, err
+ //}
// different log granularity and a custom output
machine.SetLogLevel(am.LogChanges)
@@ -236,7 +218,7 @@ func FileProcessingFlow(ctx context.Context, log Logger, filename string) (*am.M
}
// start it up!
- machine.Add(am.S{"DownloadingFile"}, am.A{"filename": filename})
+ machine.Add1(ssf.DownloadingFile, am.A{"filename": filename})
// DownloadingFile to FileUploaded
log("waiting: DownloadingFile to FileUploaded")
@@ -245,7 +227,7 @@ func FileProcessingFlow(ctx context.Context, log Logger, filename string) (*am.M
return machine, errors.New("timeout")
case <-machine.WhenErr(nil):
return machine, machine.Err
- case <-machine.When(am.S{"FileUploaded"}, nil):
+ case <-machine.When1(ssf.FileUploaded, nil):
}
return machine, nil
diff --git a/examples/temporal-fileprocessing/fileprocessing_test.go b/examples/temporal-fileprocessing/fileprocessing_test.go
index 9837ff6..54313c5 100644
--- a/examples/temporal-fileprocessing/fileprocessing_test.go
+++ b/examples/temporal-fileprocessing/fileprocessing_test.go
@@ -2,14 +2,27 @@ package temporal_fileprocessing
import (
"context"
+ "os"
+ "os/signal"
+ "syscall"
"testing"
am "github.com/pancsta/asyncmachine-go/pkg/machine"
)
-func TestFileProcessing(t *testing.T) {
+func TestFileProcessing(t *testing.T) { // Create a channel to receive OS signals
+ ctx, cancel := context.WithCancel(context.Background())
+
+ // handle OS exit signals
+ sigs := make(chan os.Signal, 1)
+ signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
+ go func() {
+ <-sigs
+ cancel()
+ }()
+
// start the flow and wait for the result
- machine, err := FileProcessingFlow(context.Background(), t.Logf, "foo.txt")
+ machine, err := FileProcessingFlow(ctx, t.Logf, "foo.txt")
if err != nil {
t.Fatal(err)
}
diff --git a/examples/temporal-fileprocessing/states/ss_fileprocessing.go b/examples/temporal-fileprocessing/states/ss_fileprocessing.go
new file mode 100644
index 0000000..0529986
--- /dev/null
+++ b/examples/temporal-fileprocessing/states/ss_fileprocessing.go
@@ -0,0 +1,50 @@
+package states
+
+import am "github.com/pancsta/asyncmachine-go/pkg/machine"
+
+// S is a type alias for a list of state names.
+type S = am.S
+
+// States map defines relations and properties of states.
+var States = am.Struct{
+ DownloadingFile: {Remove: GroupFileDownloaded},
+ FileDownloaded: {Remove: GroupFileDownloaded},
+ ProcessingFile: {
+ Auto: true,
+ Require: S{FileDownloaded},
+ Remove: GroupFileProcessed,
+ },
+ FileProcessed: {Remove: GroupFileProcessed},
+ UploadingFile: {
+ Auto: true,
+ Require: S{FileProcessed},
+ Remove: GroupFileUploaded,
+ },
+ FileUploaded: {Remove: GroupFileUploaded},
+}
+
+// Groups of mutually exclusive states.
+
+var (
+ GroupFileDownloaded = S{DownloadingFile, FileDownloaded}
+ GroupFileProcessed = S{ProcessingFile, FileProcessed}
+ GroupFileUploaded = S{UploadingFile, FileUploaded}
+)
+
+//#region boilerplate defs
+
+// Names of all the states (pkg enum).
+
+const (
+ DownloadingFile = "DownloadingFile"
+ FileDownloaded = "FileDownloaded"
+ ProcessingFile = "ProcessingFile"
+ FileProcessed = "FileProcessed"
+ UploadingFile = "UploadingFile"
+ FileUploaded = "FileUploaded"
+)
+
+// Names is an ordered list of all the state names.
+var Names = S{DownloadingFile, FileDownloaded, ProcessingFile, FileProcessed, UploadingFile, FileUploaded}
+
+//#endregion
diff --git a/examples/watcher/states/ss_watcher.go b/examples/watcher/states/ss_watcher.go
new file mode 100644
index 0000000..7b70a23
--- /dev/null
+++ b/examples/watcher/states/ss_watcher.go
@@ -0,0 +1,73 @@
+package states
+
+import am "github.com/pancsta/asyncmachine-go/pkg/machine"
+
+// S is a type alias for a list of state names.
+type S = am.S
+
+// States map defines relations and properties of states (for files).
+var States = am.Struct{
+ Init: {
+ Add: S{Watching},
+ },
+ Watching: {
+ Add: S{Init},
+ After: S{Init},
+ },
+ ChangeEvent: {
+ Multi: true,
+ Require: S{Watching},
+ },
+ Refreshing: {
+ Multi: true,
+ Remove: S{AllRefreshed},
+ },
+ Refreshed: {
+ Multi: true,
+ },
+ AllRefreshed: {},
+}
+
+// StatesDir map defines relations and properties of states (for directories).
+var StatesDir = am.Struct{
+ Refreshing: {
+ Remove: groupRefreshed,
+ },
+ Refreshed: {
+ Remove: groupRefreshed,
+ },
+ DirDebounced: {
+ Remove: groupRefreshed,
+ },
+ DirCached: {},
+}
+
+// Groups of mutually exclusive states.
+
+var groupRefreshed = S{Refreshing, Refreshed, DirDebounced}
+
+//#region boilerplate defs
+
+// Names of all the states (pkg enum).
+
+const (
+ Init = "Init"
+ Watching = "Watching"
+ ChangeEvent = "ChangeEvent"
+ Refreshing = "Refreshing"
+ Refreshed = "Refreshed"
+ AllRefreshed = "AllRefreshed"
+
+ // dir-only states
+
+ DirDebounced = "DirDebounced"
+ DirCached = "DirCached"
+)
+
+// Names is an ordered list of all the state names for files.
+var Names = S{Init, Watching, ChangeEvent, Refreshing, Refreshed, AllRefreshed}
+
+// NamesDir is an ordered list of all the state names for directories.
+var NamesDir = S{Refreshing, Refreshed, DirDebounced, DirCached}
+
+//#endregion
diff --git a/examples/watcher/watcher.go b/examples/watcher/watcher.go
new file mode 100644
index 0000000..c82c7aa
--- /dev/null
+++ b/examples/watcher/watcher.go
@@ -0,0 +1,378 @@
+package watcher
+
+import (
+ "context"
+ "log"
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+ "time"
+
+ ss "github.com/pancsta/asyncmachine-go/examples/watcher/states"
+ am "github.com/pancsta/asyncmachine-go/pkg/machine"
+ "github.com/pancsta/asyncmachine-go/pkg/telemetry"
+
+ "github.com/fsnotify/fsnotify"
+)
+
+// PathWatcher watches all dirs in PATH for changes and returns a list
+// of executables.
+type PathWatcher struct {
+ am.ExceptionHandler
+
+ Mach *am.Machine
+ ResultsLock sync.Mutex
+ Results []string
+ EnvPath string
+
+ watcher *fsnotify.Watcher
+ dirCache map[string][]string
+ dirState map[string]*am.Machine
+ ongoing map[string]context.Context
+ lastRefresh map[string]time.Time
+}
+
+func New(ctx context.Context) (*PathWatcher, error) {
+ w := &PathWatcher{
+ EnvPath: os.Getenv("PATH"),
+ dirCache: make(map[string][]string),
+ dirState: make(map[string]*am.Machine),
+ ongoing: make(map[string]context.Context),
+ lastRefresh: make(map[string]time.Time),
+ }
+ opts := &am.Opts{
+ ID: "watcher",
+ }
+
+ if os.Getenv("YAST_DEBUG") != "" {
+ opts.HandlerTimeout = time.Minute
+ opts.DontPanicToException = true
+ }
+ w.Mach = am.New(ctx, ss.States, opts)
+
+ err := w.Mach.VerifyStates(ss.Names)
+ if err != nil {
+ return nil, err
+ }
+
+ err = w.Mach.BindHandlers(w)
+ if err != nil {
+ return nil, err
+ }
+
+ w.Mach.SetTestLogger(log.Printf, am.LogChanges)
+ if os.Getenv("YAST_DEBUG") != "" {
+ err = telemetry.TransitionsToDBG(w.Mach, "")
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return w, nil
+}
+
+func (w *PathWatcher) InitState(e *am.Event) {
+ var err error
+
+ w.watcher, err = fsnotify.NewWatcher()
+ if err != nil {
+ w.Mach.Remove1(ss.Init, nil)
+ w.Mach.AddErr(err)
+ }
+}
+
+func (w *PathWatcher) InitEnd(e *am.Event) {
+ w.watcher.Close()
+}
+
+func (w *PathWatcher) WatchingState(e *am.Event) {
+ path := w.EnvPath
+ dirs := strings.Split(path, string(os.PathListSeparator))
+
+ // start the loop (bound to this instance)
+ ctx := e.Machine.NewStateCtx(ss.Watching)
+ go w.watchLoop(ctx)
+
+ // subscribe
+ for _, dirName := range dirs {
+
+ // if path doesn't exist, continue
+ if _, err := os.Stat(dirName); os.IsNotExist(err) {
+ continue
+ }
+
+ // create state per dir
+ err := w.watcher.Add(dirName)
+ if err != nil {
+ e.Machine.AddErr(err)
+ }
+
+ // create a state for each dir
+ state := am.New(ctx, ss.StatesDir, nil)
+ err = state.VerifyStates(ss.NamesDir)
+ if err != nil {
+ e.Machine.AddErr(err)
+ continue
+ }
+
+ w.dirState[dirName] = state
+
+ // schedule a refresh
+ w.Mach.Add1(ss.Refreshing, am.A{"dirName": dirName})
+ }
+}
+
+func (w *PathWatcher) WatchingEnd(e *am.Event) {
+ paths := w.watcher.WatchList()
+
+ for _, path := range paths {
+ err := w.watcher.Remove(path)
+ if err != nil {
+ e.Machine.AddErr(err)
+ }
+ }
+}
+
+func (w *PathWatcher) watchLoop(ctx context.Context) {
+ for {
+ select {
+
+ case event, ok := <-w.watcher.Events:
+ if !ok {
+ w.Mach.Remove1(ss.Watching, nil)
+ return
+ }
+ w.Mach.Add1(ss.ChangeEvent, am.A{
+ "fsnotify.Event": event,
+ })
+
+ case err, ok := <-w.watcher.Errors:
+ if !ok {
+ w.Mach.Remove1(ss.Watching, nil)
+ return
+ }
+ w.Mach.AddErr(err)
+
+ case <-ctx.Done():
+ // state expired
+ return
+ }
+ }
+}
+
+func (w *PathWatcher) ChangeEventState(e *am.Event) {
+ defer e.Machine.Remove1(ss.ChangeEvent, nil)
+ event := e.Args["fsnotify.Event"].(fsnotify.Event)
+
+ // exe
+ isRemove := event.Op&fsnotify.Remove == fsnotify.Remove
+ if !isRemove {
+ isExe, err := isExecutable(event.Name)
+ if !isExe || err != nil {
+ return
+ }
+ }
+ dirName := filepath.Dir(event.Name)
+
+ w.Mach.Add1(ss.Refreshing, am.A{
+ "dirName": dirName,
+ })
+}
+
+func (w *PathWatcher) ExceptionState(e *am.Event) {
+ w.ExceptionHandler.ExceptionState(e)
+}
+
+func (w *PathWatcher) RefreshingEnter(e *am.Event) bool {
+ // validate req params
+ _, ok1 := e.Args["dirName"]
+ dirName, ok2 := e.Args["dirName"].(string)
+ dirState, ok3 := w.dirState[dirName]
+ depsOk := ok1 && ok2 && ok3
+ if !depsOk {
+ return false
+ }
+
+ // let the debounced refreshes pass
+ isDebounce, _ := e.Args["isDebounce"].(bool)
+ if dirState.Is1(ss.Refreshing) || (dirState.Is1(ss.DirDebounced) && !isDebounce) {
+ return false
+ }
+
+ return true
+}
+
+func (w *PathWatcher) RefreshingState(e *am.Event) {
+ w.Mach.Remove1(ss.Refreshing, nil)
+
+ dirName := e.Args["dirName"].(string)
+ dirState := w.dirState[dirName]
+ // TODO config
+ debounce := time.Second
+
+ // max 1 refresh per second
+ since := time.Since(w.lastRefresh[dirName])
+ shouldDebounce := since < debounce
+ if dirState.Is1(ss.DirCached) && shouldDebounce {
+ w.Mach.Log("Debounce for %s", dirName)
+ dirState.Add1(ss.DirDebounced, nil)
+
+ go func() {
+ time.Sleep(debounce)
+ w.Mach.Add1(ss.Refreshing, am.A{
+ "dirName": dirName,
+ "isDebounce": true,
+ })
+ }()
+
+ return
+ }
+
+ w.Mach.Log("Refreshing execs in %s", dirName)
+ dirState.Add1(ss.Refreshing, nil)
+ w.ongoing[dirName] = dirState.NewStateCtx(ss.Refreshing)
+ ctx := w.ongoing[dirName]
+
+ go func() {
+ if ctx.Err() != nil {
+ return // expired
+ }
+
+ executables, err := listExecutables(dirName)
+ if err != nil {
+ e.Machine.AddErr(err)
+ }
+
+ w.Mach.Remove1(ss.Refreshing, am.A{
+ "dirName": dirName,
+ })
+ w.Mach.Add1(ss.Refreshed, am.A{
+ "dirName": dirName,
+ "executables": executables,
+ })
+ }()
+}
+
+func (w *PathWatcher) RefreshingExit(e *am.Event) bool {
+ // GC
+ _, ok := e.Args["dirName"]
+ if ok {
+ dirName, ok := e.Args["dirName"].(string)
+ if ok {
+ delete(w.ongoing, dirName)
+ }
+ }
+
+ // check completions
+ mut := e.Mutation()
+
+ // removing Init is a force shutdown
+ removeInit := mut.Type == am.MutationRemove && mut.StateWasCalled(ss.Init)
+
+ return len(w.ongoing) == 0 || removeInit
+}
+
+func (w *PathWatcher) RefreshingEnd(e *am.Event) {
+ // forced cleanup
+ for i := range w.ongoing {
+ delete(w.ongoing, i)
+ }
+}
+
+func (w *PathWatcher) RefreshedEnter(e *am.Event) bool {
+ // validate req params
+ _, ok1 := e.Args["dirName"].(string)
+ _, ok2 := e.Args["executables"].([]string)
+
+ return ok1 && ok2
+}
+
+func (w *PathWatcher) RefreshedState(e *am.Event) {
+ w.Mach.Remove1(ss.Refreshed, nil)
+
+ dirName := e.Args["dirName"].(string)
+ executables := e.Args["executables"].([]string)
+ w.dirCache[dirName] = executables
+ w.lastRefresh[dirName] = time.Now()
+
+ // update the per-dir state
+ w.dirState[dirName].Add(am.S{ss.Refreshed, ss.DirCached}, nil)
+
+ // try to finish the whole refresh
+ w.Mach.Add1(ss.AllRefreshed, nil)
+}
+
+func (w *PathWatcher) AllRefreshedEnter(e *am.Event) bool {
+ return len(w.ongoing) == 0
+}
+
+func (w *PathWatcher) AllRefreshedState(e *am.Event) {
+ w.ResultsLock.Lock()
+ defer w.ResultsLock.Unlock()
+
+ for _, executables := range w.dirCache {
+ w.Results = append(w.Results, executables...)
+ }
+ w.Results = uniqueStrings(w.Results)
+}
+
+func (w *PathWatcher) Start() {
+ w.Mach.Add1(ss.Init, nil)
+}
+
+func (w *PathWatcher) Stop() {
+ w.Mach.Remove1(ss.Init, nil)
+}
+
+///// HELPERS /////
+
+func isExecutable(path string) (bool, error) {
+ info, err := os.Stat(path)
+ if err != nil {
+ return false, err
+ }
+
+ return info.Mode().Perm()&0o111 != 0, nil
+}
+
+func listExecutables(dirPath string) ([]string, error) {
+ files, err := os.ReadDir(dirPath)
+ if err != nil {
+ return nil, err
+ }
+
+ var executables []string
+ for _, file := range files {
+ if file.IsDir() {
+ continue
+ }
+
+ fullPath := dirPath + "/" + file.Name()
+ isExe, err := isExecutable(fullPath)
+ if err != nil {
+ continue
+ }
+
+ if isExe {
+ executables = append(executables, file.Name())
+ }
+ }
+
+ return executables, nil
+}
+
+func uniqueStrings(s []string) []string {
+ seen := make(map[string]struct{})
+ var result []string
+
+ for _, v := range s {
+ if _, ok := seen[v]; ok {
+ continue
+ }
+ seen[v] = struct{}{}
+ result = append(result, v)
+ }
+
+ return result
+}
diff --git a/go.mod b/go.mod
index 147cb3e..8905441 100644
--- a/go.mod
+++ b/go.mod
@@ -4,36 +4,47 @@ go 1.21
require (
code.rocketnine.space/tslocum/cbind v0.1.5
+ github.com/dsnet/compress v0.0.1
+ github.com/fsnotify/fsnotify v1.7.0
github.com/gdamore/tcell/v2 v2.7.0
- github.com/google/uuid v1.5.0
github.com/hibiken/asynq v0.24.1
github.com/lithammer/dedent v1.1.0
- github.com/pancsta/cview v1.5.10
+ github.com/pancsta/cview v1.5.11
+ github.com/prometheus/client_golang v1.19.0
github.com/samber/lo v1.39.0
+ github.com/soheilhy/cmux v0.1.5
github.com/spf13/cobra v1.8.0
github.com/stretchr/testify v1.8.4
+ go.opentelemetry.io/otel v1.24.0
+ go.opentelemetry.io/otel/trace v1.24.0
+ golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17
+ golang.org/x/text v0.14.0
)
require (
+ github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/gdamore/encoding v1.0.0 // indirect
- github.com/golang/protobuf v1.5.2 // indirect
+ github.com/golang/protobuf v1.5.3 // indirect
+ github.com/google/uuid v1.5.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
+ github.com/prometheus/client_model v0.5.0 // indirect
+ github.com/prometheus/common v0.48.0 // indirect
+ github.com/prometheus/procfs v0.12.0 // indirect
github.com/redis/go-redis/v9 v9.0.3 // indirect
github.com/rivo/uniseg v0.4.7-0.20240127222946-601bbb3750c2 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/spf13/cast v1.3.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
- golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
+ golang.org/x/net v0.20.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/term v0.16.0 // indirect
- golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
- google.golang.org/protobuf v1.26.0 // indirect
+ google.golang.org/protobuf v1.32.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/go.sum b/go.sum
index 68c070d..f75f60b 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,7 @@
code.rocketnine.space/tslocum/cbind v0.1.5 h1:i6NkeLLNPNMS4NWNi3302Ay3zSU6MrqOT+yJskiodxE=
code.rocketnine.space/tslocum/cbind v0.1.5/go.mod h1:LtfqJTzM7qhg88nAvNhx+VnTjZ0SXBJtxBObbfBWo/M=
-code.rocketnine.space/tslocum/cview v1.5.9/go.mod h1:nraCJ41xU9AOAP/pJDV5t2fOelYp8CRGBZr8ZbEzw0w=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao=
github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w=
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
@@ -13,17 +14,24 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
+github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
+github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
+github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
+github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
+github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.2.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
github.com/gdamore/tcell/v2 v2.7.0 h1:I5LiGTQuwrysAt1KS9wg1yFfOI3arI3ucFrxtd/xqaA=
github.com/gdamore/tcell/v2 v2.7.0/go.mod h1:hl/KtAANGBecfIPxk+FzKvThTqI84oplgbPEmVX60b8=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -31,8 +39,11 @@ github.com/hibiken/asynq v0.24.1 h1:+5iIEAyA9K/lcSPvx3qoPtsKJeKI5u9aOIvUmSsazEw=
github.com/hibiken/asynq v0.24.1/go.mod h1:u5qVeSbrnfT+vtG5Mq8ZPzQu/BmCKMHvTGb91uy9Tts=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
-github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
+github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
+github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
@@ -44,10 +55,18 @@ github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
-github.com/pancsta/cview v1.5.10 h1:Lmj5+klFAAubvd3jAjpmPFW4aQfh+FUtR8t5kFH7IDQ=
-github.com/pancsta/cview v1.5.10/go.mod h1:BkP0JtxfBHYZuc4CmK/O5H5fNCEdGAgndVeJlGiS3TQ=
+github.com/pancsta/cview v1.5.11 h1:ocOabbHPzNCOnLPcNdHqnG/1KfT/UaFjfFNg2Jpsyjw=
+github.com/pancsta/cview v1.5.11/go.mod h1:BkP0JtxfBHYZuc4CmK/O5H5fNCEdGAgndVeJlGiS3TQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
+github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
+github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
+github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
+github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
+github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
+github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
+github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/redis/go-redis/v9 v9.0.3 h1:+7mmR26M0IvyLxGZUHxu4GiBkJkVDid0Un+j4ScYu4k=
github.com/redis/go-redis/v9 v9.0.3/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -58,9 +77,13 @@ github.com/rivo/uniseg v0.4.7-0.20240127222946-601bbb3750c2 h1:tcc3ZFBvjydcgrAxa
github.com/rivo/uniseg v0.4.7-0.20240127222946-601bbb3750c2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
+github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
+github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA=
github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
+github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js=
+github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
@@ -72,12 +95,18 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
+go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
+go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
+go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA=
go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM=
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
@@ -88,16 +117,20 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
+golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210309040221-94ec62e08169/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -136,15 +169,16 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
-google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
+google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/pkg/history/history.go b/pkg/history/history.go
new file mode 100644
index 0000000..67e25ca
--- /dev/null
+++ b/pkg/history/history.go
@@ -0,0 +1,179 @@
+// Package history provides a basic history tracker for asyncmachine, along with
+// some utilities to query the log.
+package history
+
+import (
+ "slices"
+ "sync"
+ "time"
+
+ am "github.com/pancsta/asyncmachine-go/pkg/machine"
+ "github.com/pancsta/asyncmachine-go/pkg/x/helpers"
+)
+
+type History struct {
+ Entries []Entry
+ // LastActivated is a map of state names to the last time they were activated
+ LastActivated map[string]time.Time
+ // tracked states
+ States am.S
+
+ mx sync.Mutex
+ maxEntries int
+}
+
+func (h *History) TransitionInit(transition *am.Transition) {}
+func (h *History) HandlerStart(transition *am.Transition, emitter string,
+ handler string) {
+}
+
+func (h *History) HandlerEnd(transition *am.Transition, emitter string,
+ handler string) {
+}
+func (h *History) End() {}
+func (h *History) MachineInit(mach *am.Machine) {}
+func (h *History) NewSubmachine(parent, mach *am.Machine) {}
+func (h *History) Inheritable() bool {
+ return false
+}
+func (h *History) MachineDispose(machID string) {}
+
+func (h *History) TransitionEnd(tx *am.Transition) {
+ if !tx.Accepted {
+ return
+ }
+ mut := tx.Mutation
+ match := false
+ for _, name := range h.States {
+ if mut.StateWasCalled(name) {
+ match = true
+ break
+ }
+ }
+ if !match {
+ return
+ }
+ // rotate TODO optimize rotation
+ if len(h.Entries) >= h.maxEntries {
+ cutFrom := len(h.Entries) - h.maxEntries
+ h.Entries = h.Entries[cutFrom:]
+ }
+ // remember this mutation, remove Args
+ h.Entries = append(h.Entries, Entry{
+ CalledStates: helpers.StatesToIndexes(tx.Machine, tx.Mutation.CalledStates),
+ Type: tx.Mutation.Type,
+ Auto: tx.Mutation.Auto,
+ })
+ h.mx.Lock()
+ // update last seen time
+ for _, name := range tx.TargetStates {
+ if !slices.Contains(h.States, name) {
+ continue
+ }
+ h.LastActivated[name] = time.Now()
+ }
+ h.mx.Unlock()
+}
+
+type Entry struct {
+ // Mutation type
+ Type am.MutationType
+ // Indexes of called states
+ CalledStates []int
+ // Auto is true if the mutation was triggered by an Auto state
+ Auto bool
+ // T machine time (logical clocks)
+ T am.T
+ // Time real time
+ Time time.Time
+}
+
+type MatcherStates struct {
+ // Called is a set of states that were called in the mutation
+ Called am.S
+ // Active is a set of states that were active AFTER the mutation
+ Active am.S
+ // Inactive is a set of states that were inactive AFTER the mutation
+ Inactive am.S
+}
+
+type MatcherTimes struct {
+ // MinT is the minimum machine time the mutation could have occurred
+ MinT am.T
+ // MaxT is the maximum machine time the mutation could have occurred
+ MaxT am.T
+ // MinReal is the minimum real time the mutation could have occurred
+ MinReal time.Time
+ // MaxReal is the maximum real time the mutation could have occurred
+ MaxReal time.Time
+}
+
+type MatcherIndexes struct {
+ // Start is the minimum index of the mutation in the history
+ Start int
+ // End is the maximum index of the mutation in the history
+ End int
+}
+
+// ActivatedRecently returns true if the state was activated within the last
+// duration.
+func (h *History) ActivatedRecently(state string, duration time.Duration) bool {
+ h.mx.Lock()
+ defer h.mx.Unlock()
+ last, ok := h.LastActivated[state]
+ if !ok {
+ return false
+ }
+ return time.Since(last) < duration
+}
+
+// MatchEntries returns all entries that match the given criteria.
+// TODO MatchEntries
+func (h *History) MatchEntries(
+ states MatcherStates, times MatcherTimes, indexes MatcherIndexes,
+) []Entry {
+ panic("not implemented")
+}
+
+// StatesActiveDuring returns true if all the given states were active during
+// the whole given time range.
+// TODO StatesActiveDuring
+func (h *History) StatesActiveDuring(
+ states am.S, timeRange MatcherTimes,
+) bool {
+ panic("not implemented")
+}
+
+// StatesInactiveDuring returns true if all the given states were inactive
+// during the whole given time range.
+// TODO StatesInactiveDuring
+func (h *History) StatesInactiveDuring(
+ states am.S, timeRange MatcherTimes,
+) bool {
+ panic("not implemented")
+}
+
+// Track creates a new history tracer for the given machine and states. Bind the
+// tracker isnt thread safe and can't be unbound afterward.
+// maxEntries: the maximum number of entries to keep in the history, 0 for
+// default.
+// TODO add MaxLimits{Entries int, Age time.Duration, MachineAge: uint64}
+func Track(mach *am.Machine, states am.S, maxEntries int) *History {
+ if maxEntries <= 0 {
+ maxEntries = 1000
+ }
+ history := &History{
+ Entries: []Entry{},
+ LastActivated: map[string]time.Time{},
+ States: states,
+ maxEntries: maxEntries,
+ }
+ // mark active states as activated to reflect the current state
+ for _, name := range states {
+ if mach.Is1(name) {
+ history.LastActivated[name] = time.Now()
+ }
+ }
+ mach.Tracers = append(mach.Tracers, history)
+ return history
+}
diff --git a/pkg/history/history_test.go b/pkg/history/history_test.go
new file mode 100644
index 0000000..37e0c98
--- /dev/null
+++ b/pkg/history/history_test.go
@@ -0,0 +1,84 @@
+package history
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+
+ am "github.com/pancsta/asyncmachine-go/pkg/machine"
+)
+
+func ExampleTrack() {
+ // create a machine
+ mach := am.New(context.Background(), am.Struct{
+ "A": {},
+ "B": {},
+ }, nil)
+
+ // create a history
+ h := Track(mach, am.S{"A", "C"}, 0)
+
+ // collect some info
+ h.ActivatedRecently("A", time.Second) // true
+ fmt.Printf("%s", h.LastActivated["A"])
+ println(h.Entries)
+}
+
+func TestTrack(t *testing.T) {
+ // create a machine
+ mach := NewNoRels(t, nil)
+
+ // create a history
+ h := Track(mach, am.S{"A", "C"}, 0)
+
+ // mutate
+ mach.Add(am.S{"A", "B"}, nil)
+ lastA := h.LastActivated["A"]
+ mach.Add1("C", nil)
+ mach.Remove1("A", nil)
+ time.Sleep(time.Nanosecond)
+ mach.Add(am.S{"A", "D"}, nil)
+
+ // assert
+ assert.Greater(t, h.LastActivated["A"].Nanosecond(), lastA.Nanosecond(),
+ "Activation timestamp updated")
+ assert.Len(t, h.Entries, 4, "4 mutations for tracked states")
+ assert.True(t, h.ActivatedRecently("A", time.Second), "A was activated")
+ assert.False(t, h.ActivatedRecently("B", time.Second), "B isn't tracked")
+}
+
+func TestTrackPreactivation(t *testing.T) {
+ // create a machine
+ mach := NewNoRels(t, am.S{"A", "C"})
+
+ // create a history
+ h := Track(mach, am.S{"A", "C"}, 0)
+
+ // assert
+ assert.True(t, h.ActivatedRecently("A", time.Second), "A was pre-activated")
+}
+
+// NewNoRels creates a new machine with no relations between states.
+func NewNoRels(t *testing.T, initialState am.S) *am.Machine {
+ m := am.New(context.Background(), am.Struct{
+ "A": {},
+ "B": {},
+ "C": {},
+ "D": {},
+ }, nil)
+ m.SetLogger(func(i am.LogLevel, msg string, args ...any) {
+ t.Logf(msg, args...)
+ })
+ if os.Getenv("AM_DEBUG") != "" {
+ m.SetLogLevel(am.LogEverything)
+ m.HandlerTimeout = 2 * time.Minute
+ }
+ if initialState != nil {
+ m.Set(initialState, nil)
+ }
+ return m
+}
diff --git a/pkg/machine/machine.go b/pkg/machine/machine.go
index 89fe874..f77cf18 100644
--- a/pkg/machine/machine.go
+++ b/pkg/machine/machine.go
@@ -1,41 +1,37 @@
-// Package machine is a minimal implementation of AsyncMachine [1] in Golang
+// Package machine is a dependency-free implementation of AsyncMachine in Golang
// using channels and context. It aims at simplicity and speed.
//
-// It can be used as a lightweight in-memory Temporal [2] alternative, worker
-// for Asynq [3], or to write simple consensus engines, stateful firewalls,
+// It can be used as a lightweight in-memory Temporal [1] alternative, worker
+// for Asynq [2], or to write simple consensus engines, stateful firewalls,
// telemetry, bots, etc.
//
-// AsyncMachine is a relational state machine which never blocks.
+// asyncmachine-go is a general purpose state machine for managing complex
+// async workflows in a safe and structured way.
//
-// [1]: https://github.com/TobiaszCudnik/asyncmachine
-// [2]: https://github.com/temporalio/temporal
-// [3]: https://github.com/hibiken/asynq
-package machine
+// [1]: https://github.com/temporalio/temporal
+// [2]: https://github.com/hibiken/asynq
+package machine // import "github.com/pancsta/asyncmachine-go/pkg/machine"
import (
"context"
"errors"
"fmt"
+ "maps"
+ "os"
"reflect"
"runtime/debug"
"slices"
"strings"
"sync"
- "sync/atomic"
"time"
-
- "github.com/google/uuid"
- "github.com/samber/lo"
)
// Machine represent states, provides mutation methods, helpers methods and
// info about the current and scheduled transitions (if any).
-// See https://github.com/pancsta/asyncmachine-go/blob/main/docs/manual.md
type Machine struct {
- // Unique ID of this machine. Default: random UUID.
+ // Unique ID of this machine. Default: random ID.
ID string
// Time for a handler to execute. Default: time.Second
- // TODO support
HandlerTimeout time.Duration
// If true, the machine will print all exceptions to stdout. Default: true.
// Requires an ExceptionHandler binding and Machine.PanicToException set.
@@ -43,75 +39,146 @@ type Machine struct {
// If true, the machine will catch panic and trigger the Exception state.
// Default: true.
PanicToException bool
- // If true, the machine will prefix its logs with the machine ID (5 chars).
+ // If true, logs will start with machine's ID (5 chars).
// Default: true.
LogID bool
// Relations resolver, used to produce target states of a transition.
- // Default: *DefaultRelationsResolver.
+ // Default: *DefaultRelationsResolver. Read-only.
Resolver RelationsResolver
- // Ctx is the context of the machine.
+ // Ctx is the context of the machine. Read-only.
Ctx context.Context
- // Err is the last error that occurred.
+ // Err is the last error that occurred. Read-only.
Err error
- // States is a map of state names to state definitions.
- States States
- // Queue of mutations to be executed.
- Queue []Mutation
- // Currently executing transition (if any).
+ // Maximum number of mutations that can be queued. Default: 1000.
+ QueueLimit int
+ // Currently executing transition (if any). Read-only.
Transition *Transition
- // All states that are currently active.
- ActiveStates S
- // List of all the registered state names.
+ // List of all the registered state names. Read-only.
StateNames S
-
- emitters []*emitter
- clock Clocks
- cancel context.CancelFunc
- logLevel LogLevel
- logger Logger
- queueLock sync.RWMutex
- queueProcessing atomic.Value
+ // Confirms the states have been ordered using VerifyStates. Read-only.
+ StatesVerified bool
+ // Tracers are optional tracers for telemetry integrations.
+ Tracers []Tracer
+ // If true, the machine has been Disposed and is no-op. Read-only.
+ Disposed bool
+ // Channel closing when the machine finished disposal. Read-only.
+ WhenDisposed chan struct{}
+ // ParentID is the ID of the parent machine (if any).
+ ParentID string
+ // Tags are optional tags for telemetry integrations etc.
+ Tags []string
+
+ // states is a map of state names to state definitions.
+ states Struct
+ statesLock sync.RWMutex
+ // activeStates is list of currently active states.
+ activeStates S
activeStatesLock sync.RWMutex
- panicCaught bool
- disposed bool
- indexWhen indexWhen
- indexStateCtx indexStateCtx
- indexEventCh indexEventCh
- indexEventChLock sync.Mutex
- handlerDone chan bool
- handlerPanic chan any
- logEntriesLock sync.Mutex
- logEntries []string
+ // queue of mutations to be executed.
+ queue []*Mutation
+ queueLock sync.RWMutex
+ queueProcessing sync.Mutex
+ queueLen int
+
+ emitters []*emitter
+ once sync.Once
+ clock Clocks
+ cancel context.CancelFunc
+ logLevel LogLevel
+ logger Logger
+ panicCaught bool
+ // unlockDisposed means that disposal is in progress and holding the queueLock
+ unlockDisposed bool
+ indexWhen indexWhen
+ indexWhenTime indexWhenTime
+ indexWhenArgs indexWhenArgs
+ indexWhenArgsLock sync.Mutex
+ indexStateCtx indexStateCtx
+ indexEventCh indexEventCh
+ indexEventChLock sync.Mutex
+ handlerStart chan *handlerCall
+ handlerEnd chan bool
+ handlerPanic chan any
+ handlerTimer *time.Timer
+ handlerLoopRunning bool
+ logEntriesLock sync.Mutex
+ logEntries []string
+ logArgs func(args A) map[string]string
+ currentHandler string
+ disposeHandlers []func()
}
-// New creates a new Machine instance, bound to context and modified with
-// optional Opts
-func New(ctx context.Context, states States, opts *Opts) *Machine {
- // parse relations
- parsedStates := cloneStates(states)
- for name, state := range states {
- // avoid self removal
- if lo.Contains(state.Remove, name) {
- state.Remove = lo.Without(state.Remove, name)
- parsedStates[name] = state
+// NewCommon creates a new Machine instance with all the common options set.
+func NewCommon(
+ ctx context.Context, id string, statesStruct Struct, stateNames S,
+ handlers any, parent *Machine, opts *Opts,
+) (*Machine, error) {
+ machOpts := &Opts{ID: id}
+
+ if opts != nil {
+ machOpts = opts
+ machOpts.ID = id
+ }
+
+ if os.ExpandEnv("AM_DEBUG") != "" {
+ machOpts = OptsWithDebug(machOpts)
+ if parent != nil {
+ machOpts = OptsWithParentTracers(machOpts, parent)
}
}
+
+ if parent != nil {
+ machOpts.Parent = parent
+ }
+
+ mach := New(ctx, statesStruct, machOpts)
+ err := mach.VerifyStates(stateNames)
+ if err != nil {
+ return nil, err
+ }
+
+ if handlers != nil {
+ err = mach.BindHandlers(handlers)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return mach, nil
+}
+
+// New creates a new Machine instance, bound to context and modified with
+// optional Opts.
+func New(ctx context.Context, statesStruct Struct, opts *Opts) *Machine {
+ // parse relations
+ parsedStates := parseStruct(statesStruct)
+
m := &Machine{
- ID: uuid.New().String(),
- HandlerTimeout: time.Second,
- States: parsedStates,
+ ID: randID(),
+ HandlerTimeout: 100 * time.Millisecond,
+ states: parsedStates,
clock: map[string]uint64{},
emitters: []*emitter{},
PrintExceptions: true,
PanicToException: true,
LogID: true,
+ QueueLimit: 1000,
indexWhen: indexWhen{},
+ indexWhenTime: indexWhenTime{},
+ indexWhenArgs: indexWhenArgs{},
indexStateCtx: indexStateCtx{},
indexEventCh: indexEventCh{},
- handlerDone: make(chan bool),
+ handlerStart: make(chan *handlerCall),
+ handlerEnd: make(chan bool),
handlerPanic: make(chan any),
+ handlerTimer: time.NewTimer(24 * time.Hour),
+ WhenDisposed: make(chan struct{}),
}
- m.queueProcessing.Store(false)
+
+ // parse opts
+ // TODO extract
+ opts = cloneOptions(opts)
+ var parent *Machine
if opts != nil {
if opts.ID != "" {
m.ID = opts.ID
@@ -134,50 +201,136 @@ func New(ctx context.Context, states States, opts *Opts) *Machine {
if opts.LogLevel != LogNothing {
m.SetLogLevel(opts.LogLevel)
}
+ if opts.Tracers != nil {
+ m.Tracers = opts.Tracers
+ }
+ if opts.LogArgs != nil {
+ m.logArgs = opts.LogArgs
+ }
+ if opts.QueueLimit != 0 {
+ m.QueueLimit = opts.QueueLimit
+ }
+ parent = opts.Parent
+ m.ParentID = opts.ParentID
+ m.Tags = opts.Tags
}
+
// default resolver
if m.Resolver == nil {
m.Resolver = &DefaultRelationsResolver{
Machine: m,
}
}
+
// define the exception state (if missing)
- if _, ok := m.States["Exception"]; !ok {
- m.States["Exception"] = State{
+ if _, ok := m.states[Exception]; !ok {
+ m.states[Exception] = State{
Multi: true,
}
}
- for name := range m.States {
+
+ // infer and sort states for defaults
+ for name := range m.states {
m.StateNames = append(m.StateNames, name)
slices.Sort(m.StateNames)
m.clock[name] = 0
}
+
// init context
- m.Ctx, m.cancel = context.WithCancel(ctx)
- go func() {
- <-m.Ctx.Done()
- if m.Err == nil {
- m.Err = m.Ctx.Err()
+ if ctx == nil {
+ panic("machine ctx is required")
+ }
+ if parent != nil {
+ m.ParentID = parent.ID
+
+ // info the tracers about this being a submachine
+ for i := range parent.Tracers {
+ if !parent.Tracers[i].Inheritable() {
+ continue
+ }
+ parent.Tracers[i].NewSubmachine(parent, m)
}
- m.Dispose()
- }()
+ }
+ m.Ctx, m.cancel = context.WithCancel(ctx)
+
+ // tracers
+ for i := range m.Tracers {
+ m.Tracers[i].MachineInit(m)
+ }
return m
}
-// Dispose disposes the machine and all its emitters.
+func cloneOptions(opts *Opts) *Opts {
+ if opts == nil {
+ return &Opts{}
+ }
+
+ return &Opts{
+ ID: opts.ID,
+ HandlerTimeout: opts.HandlerTimeout,
+ DontPanicToException: opts.DontPanicToException,
+ DontPrintExceptions: opts.DontPrintExceptions,
+ DontLogID: opts.DontLogID,
+ Resolver: opts.Resolver,
+ LogLevel: opts.LogLevel,
+ Tracers: opts.Tracers,
+ LogArgs: opts.LogArgs,
+ QueueLimit: opts.QueueLimit,
+ Parent: opts.Parent,
+ ParentID: opts.ParentID,
+ Tags: opts.Tags,
+ }
+}
+
+// Dispose disposes the machine and all its emitters. You can wait for the
+// completion of the disposal with `<-mach.WhenDisposed`.
func (m *Machine) Dispose() {
- if m.disposed {
+ // dispose in a goroutine to avoid a deadlock when called from within a
+ // handler
+ if m.Disposed {
return
}
- m.disposed = true
+
+ go func() {
+ m.queueProcessing.Lock()
+ m.unlockDisposed = true
+ m.once.Do(m.dispose)
+ }()
+}
+
+// DisposeForce disposes the machine and all its emitters, without waiting for
+// the queue to drain. Will cause panics.
+func (m *Machine) DisposeForce() {
+ m.once.Do(m.dispose)
+}
+
+func (m *Machine) dispose() {
+ m.Disposed = true
+
+ for i := range m.Tracers {
+ m.Tracers[i].MachineDispose(m.ID)
+ }
+
+ m.activeStatesLock.Lock()
+ defer m.activeStatesLock.Unlock()
+ m.indexWhenArgsLock.Lock()
+ defer m.indexWhenArgsLock.Unlock()
+ m.indexEventChLock.Lock()
+ defer m.indexEventChLock.Unlock()
+
m.log(LogEverything, "[end] dispose")
m.cancel()
+ if m.Err == nil && m.Ctx.Err() != nil {
+ m.Err = m.Ctx.Err()
+ }
for _, e := range m.emitters {
m.disposeEmitter(e)
}
m.logger = nil
+
// state contexts get cancelled automatically
m.indexStateCtx = nil
+
// channels need to be closed manually
for s := range m.indexWhen {
for k := range m.indexWhen[s] {
@@ -185,78 +338,77 @@ func (m *Machine) Dispose() {
}
m.indexWhen[s] = nil
}
+ m.indexWhen = nil
+
+ for s := range m.indexWhenArgs {
+ for k := range m.indexWhen[s] {
+ closeSafe(m.indexWhenArgs[s][k].ch)
+ }
+ m.indexWhenArgs[s] = nil
+ }
+ m.indexWhenArgs = nil
+
for e := range m.indexEventCh {
for k := range m.indexEventCh[e] {
closeSafe(m.indexEventCh[e][k])
}
m.indexEventCh[e] = nil
}
- m.indexWhen = nil
+ m.indexEventCh = nil
+
+ m.Tracers = nil
+ closeSafe(m.handlerEnd)
+ closeSafe(m.handlerPanic)
+ closeSafe(m.handlerStart)
+ m.handlerTimer.Stop()
+ m.handlerTimer = nil
+ m.clock = nil
+
+ // release the queue lock
+ if m.unlockDisposed {
+ m.unlockDisposed = false
+ m.queueProcessing.Unlock()
+ }
+
+ // run dispose handlers
+ // TODO timeouts?
+ for _, fn := range m.disposeHandlers {
+ fn()
+ }
+ m.disposeHandlers = nil
+
+ // the end
+ close(m.WhenDisposed)
}
// disposeEmitter detaches the emitter from the machine and disposes it.
func (m *Machine) disposeEmitter(emitter *emitter) {
m.log(LogEverything, "[end] emitter %s", emitter.id)
- m.emitters = lo.Without(m.emitters, emitter)
+ m.emitters = slicesWithout(m.emitters, emitter)
emitter.dispose()
}
// WhenErr returns a channel that will be closed when the machine is in the
// Exception state.
+//
// ctx: optional context defaults to the machine's context.
func (m *Machine) WhenErr(ctx context.Context) chan struct{} {
// handle with a shared channel with broadcast via close
return m.When([]string{"Exception"}, ctx)
}
-// GetRelationsBetween returns a list of relation types between the given
-// states.
-func (m *Machine) GetRelationsBetween(fromState, toState string) []Relation {
- m.MustParseStates(S{fromState})
- m.MustParseStates(S{toState})
- state := m.States[fromState]
- var relations []Relation
- if state.Add != nil && lo.Contains(state.Add, toState) {
- relations = append(relations, RelationAdd)
- }
- if state.Require != nil && lo.Contains(state.Require, toState) {
- relations = append(relations, RelationRequire)
- }
- if state.Remove != nil && lo.Contains(state.Remove, toState) {
- relations = append(relations, RelationRemove)
- }
- if state.After != nil && lo.Contains(state.After, toState) {
- relations = append(relations, RelationAfter)
- }
- return relations
-}
-
-// GetRelationsOf returns a list of relation types of the given state.
-func (m *Machine) GetRelationsOf(fromState string) []Relation {
- m.MustParseStates(S{fromState})
- state := m.States[fromState]
- var relations []Relation
- if state.Add != nil {
- relations = append(relations, RelationAdd)
- }
- if state.Require != nil {
- relations = append(relations, RelationRequire)
- }
- if state.Remove != nil {
- relations = append(relations, RelationRemove)
- }
- if state.After != nil {
- relations = append(relations, RelationAfter)
- }
- return relations
-}
-
// When returns a channel that will be closed when all the passed states
// become active or the machine gets disposed.
+//
// ctx: optional context that will close the channel when done. Useful when
// listening on 2 When() channels within the same `select` to GC the 2nd one.
-func (m *Machine) When(states []string, ctx context.Context) chan struct{} {
+// TODO re-use channels with the same state set and context
+func (m *Machine) When(states S, ctx context.Context) chan struct{} {
ch := make(chan struct{})
+ if m.Disposed {
+ close(ch)
+ return ch
+ }
m.activeStatesLock.Lock()
defer m.activeStatesLock.Unlock()
@@ -275,6 +427,7 @@ func (m *Machine) When(states []string, ctx context.Context) chan struct{} {
matched++
}
}
+
// add the binding to an index of each state
binding := &whenBinding{
ch: ch,
@@ -283,12 +436,21 @@ func (m *Machine) When(states []string, ctx context.Context) chan struct{} {
total: len(states),
matched: matched,
}
+
// dispose with context
+ // TODO extract
if ctx != nil {
go func() {
- <-ctx.Done()
+ select {
+ case <-ch:
+ return
+ case <-m.Ctx.Done():
+ return
+ case <-ctx.Done():
+ }
+
// GC only if needed
- if m.disposed {
+ if m.Disposed {
return
}
@@ -297,11 +459,16 @@ func (m *Machine) When(states []string, ctx context.Context) chan struct{} {
for _, s := range states {
if _, ok := m.indexWhen[s]; ok {
- m.indexWhen[s] = lo.Without(m.indexWhen[s], binding)
+ if len(m.indexWhen[s]) == 1 {
+ delete(m.indexWhen, s)
+ } else {
+ m.indexWhen[s] = slicesWithout(m.indexWhen[s], binding)
+ }
}
}
}()
}
+
// insert the binding
for _, s := range states {
if _, ok := m.indexWhen[s]; !ok {
@@ -314,12 +481,23 @@ func (m *Machine) When(states []string, ctx context.Context) chan struct{} {
return ch
}
+// When1 is an alias to When() for a single state.
+// See When.
+func (m *Machine) When1(state string, ctx context.Context) chan struct{} {
+ return m.When(S{state}, ctx)
+}
+
// WhenNot returns a channel that will be closed when all the passed states
// become inactive or the machine gets disposed.
+//
// ctx: optional context that will close the channel when done. Useful when
// listening on 2 WhenNot() channels within the same `select` to GC the 2nd one.
-func (m *Machine) WhenNot(states []string, ctx context.Context) chan struct{} {
+func (m *Machine) WhenNot(states S, ctx context.Context) chan struct{} {
ch := make(chan struct{})
+ if m.Disposed {
+ close(ch)
+ return ch
+ }
// if all active, close early
if m.Not(states) {
@@ -338,6 +516,7 @@ func (m *Machine) WhenNot(states []string, ctx context.Context) chan struct{} {
matched++
}
}
+
// add the binding to an index of each state
binding := &whenBinding{
ch: ch,
@@ -346,12 +525,20 @@ func (m *Machine) WhenNot(states []string, ctx context.Context) chan struct{} {
total: len(states),
matched: matched,
}
+
// dispose with context
+ // TODO extract
if ctx != nil {
go func() {
- <-ctx.Done()
+ select {
+ case <-ch:
+ return
+ case <-m.Ctx.Done():
+ return
+ case <-ctx.Done():
+ }
// GC only if needed
- if m.disposed {
+ if m.Disposed {
return
}
@@ -360,11 +547,16 @@ func (m *Machine) WhenNot(states []string, ctx context.Context) chan struct{} {
for _, s := range states {
if _, ok := m.indexWhen[s]; ok {
- m.indexWhen[s] = lo.Without(m.indexWhen[s], binding)
+ if len(m.indexWhen[s]) == 1 {
+ delete(m.indexWhen, s)
+ } else {
+ m.indexWhen[s] = slicesWithout(m.indexWhen[s], binding)
+ }
}
}
}()
}
+
// insert the binding
for _, s := range states {
if _, ok := m.indexWhen[s]; !ok {
@@ -377,6 +569,203 @@ func (m *Machine) WhenNot(states []string, ctx context.Context) chan struct{} {
return ch
}
+// WhenNot1 is an alias to WhenNot() for a single state.
+// See WhenNot.
+func (m *Machine) WhenNot1(state string, ctx context.Context) chan struct{} {
+ return m.WhenNot(S{state}, ctx)
+}
+
+// WhenArgs returns a channel that will be closed when the passed state
+// becomes active with all the passed args. Args are compared using the native
+// '=='. It's meant to be used with async Multi states, to filter out
+// a specific completion.
+// TODO better val comparisons
+func (m *Machine) WhenArgs(
+ state string, args A, ctx context.Context,
+) chan struct{} {
+ ch := make(chan struct{})
+
+ if m.Disposed {
+ close(ch)
+
+ return ch
+ }
+
+ m.MustParseStates(S{state})
+ name := state + "State"
+
+ // locks
+ m.indexWhenArgsLock.Lock()
+ defer m.indexWhenArgsLock.Unlock()
+
+ // log
+ m.log(LogDecisions, "[when:args] new matcher for %s", state)
+
+ // try to reuse an existing channel
+ for _, binding := range m.indexWhenArgs[name] {
+ if compareArgs(binding.args, args) {
+ return binding.ch
+ }
+ }
+
+ binding := &whenArgsBinding{
+ ch: ch,
+ args: args,
+ }
+
+ // dispose with context
+ // TODO extract
+ if ctx != nil {
+ go func() {
+ select {
+ case <-ch:
+ return
+ case <-m.Ctx.Done():
+ return
+ case <-ctx.Done():
+ }
+ // GC only if needed
+ if m.Disposed {
+ return
+ }
+
+ m.indexWhenArgsLock.Lock()
+ defer m.indexWhenArgsLock.Unlock()
+
+ if _, ok := m.indexWhenArgs[name]; ok {
+ if len(m.indexWhenArgs[name]) == 1 {
+ delete(m.indexWhenArgs, name)
+ } else {
+ m.indexWhenArgs[name] = slicesWithout(m.indexWhenArgs[name], binding)
+ }
+ }
+ }()
+ }
+
+ // insert the binding
+ if _, ok := m.indexWhen[name]; !ok {
+ m.indexWhenArgs[name] = []*whenArgsBinding{binding}
+ } else {
+ m.indexWhenArgs[name] = append(m.indexWhenArgs[name], binding)
+ }
+
+ return ch
+}
+
+// WhenTime returns a channel that will be closed when all the passed states
+// have passed the specified time. The time is a logical clock of the state.
+// Machine time can be sourced from the Time() method, or Clock() for a specific
+// state.
+func (m *Machine) WhenTime(
+ states S, times T, ctx context.Context,
+) chan struct{} {
+ ch := make(chan struct{})
+ valid := len(states) == len(times)
+ if m.Disposed || !valid {
+ if !valid {
+ m.log(LogDecisions, "[when:time] times for all passed stated required")
+ }
+ close(ch)
+ return ch
+ }
+ m.MustParseStates(states)
+
+ m.activeStatesLock.Lock()
+ defer m.activeStatesLock.Unlock()
+ indexWhenTime := m.indexWhenTime
+
+ // if all times passed, close early
+ passed := true
+ for i, s := range states {
+ if m.clock[s] < times[i] {
+ passed = false
+ break
+ }
+ }
+ if passed {
+ close(ch)
+ return ch
+ }
+
+ completed := stateIsActive{}
+ matched := 0
+ index := map[string]int{}
+ for i, s := range states {
+ completed[s] = m.clock[s] >= times[i]
+ if completed[s] {
+ matched++
+ }
+ index[s] = i
+ }
+
+ // add the binding to an index of each state
+ binding := &whenTimeBinding{
+ ch: ch,
+ index: index,
+ completed: completed,
+ total: len(states),
+ matched: matched,
+ times: times,
+ }
+
+ // dispose with context
+ // TODO extract
+ if ctx != nil {
+ go func() {
+ select {
+ case <-ch:
+ return
+ case <-m.Ctx.Done():
+ return
+ case <-ctx.Done():
+ }
+ // GC only if needed
+ if m.Disposed {
+ return
+ }
+
+ m.activeStatesLock.Lock()
+ defer m.activeStatesLock.Unlock()
+
+ for _, s := range states {
+ if _, ok := indexWhenTime[s]; ok {
+ if len(m.indexWhenTime[s]) == 1 {
+ delete(m.indexWhenTime, s)
+ } else {
+ m.indexWhenTime[s] = slicesWithout(m.indexWhenTime[s], binding)
+ }
+ }
+ }
+ }()
+ }
+
+ // insert the binding
+ for _, s := range states {
+ if _, ok := indexWhenTime[s]; !ok {
+ indexWhenTime[s] = []*whenTimeBinding{binding}
+ } else {
+ indexWhenTime[s] = append(indexWhenTime[s], binding)
+ }
+ }
+
+ return ch
+}
+
+// WhenTicks waits N tick for a single state. Uses WhenTime underneath.
+func (m *Machine) WhenTicks(
+ state string, ticks int, ctx context.Context,
+) chan struct{} {
+ return m.WhenTime(S{state}, T{uint64(ticks) + m.Clock(state)}, ctx)
+}
+
+// WhenTicksEq waits till ticks for a single state equal the given value.
+// Uses WhenTime underneath.
+func (m *Machine) WhenTicksEq(
+ state string, ticks int, ctx context.Context,
+) chan struct{} {
+ return m.WhenTime(S{state}, T{uint64(ticks)}, ctx)
+}
+
// Time returns a list of logical clocks of specified states (or all the states
// if nil).
// states: optionally passing a list of states param guarantees a deterministic
@@ -388,36 +777,62 @@ func (m *Machine) Time(states S) T {
if states == nil {
states = m.StateNames
}
- return lo.Map(states, func(state string, i int) uint64 {
- return m.clock[state]
- })
+
+ ret := make(T, len(states))
+ for i, s := range states {
+ ret[i] = m.clock[s]
+ }
+
+ return ret
}
// TimeSum returns the sum of logical clocks of specified states (or all states
// if nil). It's a very inaccurate, yet simple way to measure the machine's
// time.
+// TODO handle overflow
func (m *Machine) TimeSum(states S) uint64 {
- return lo.Reduce(m.Time(states), func(acc uint64, clock uint64,
- i int,
- ) uint64 {
- return acc + clock
- }, 0)
+ m.activeStatesLock.RLock()
+ defer m.activeStatesLock.RUnlock()
+
+ if states == nil {
+ states = m.StateNames
+ }
+
+ ret := uint64(0)
+ for _, s := range states {
+ ret += m.clock[s]
+ }
+
+ return ret
}
// Add activates a list of states in the machine, returning the result of the
// transition (Executed, Queued, Canceled).
// Like every mutation method, it will resolve relations and trigger handlers.
func (m *Machine) Add(states S, args A) Result {
- m.queueMutation(MutationTypeAdd, states, args)
+ if m.Disposed || m.queueLen >= m.QueueLimit {
+ return Canceled
+ }
+ m.queueMutation(MutationAdd, states, args)
+
return m.processQueue()
}
+// Add1 is a shorthand method to add a single state with the passed args.
+func (m *Machine) Add1(state string, args A) Result {
+ return m.Add(S{state}, args)
+}
+
// AddErr is a shorthand method to add the Exception state with the passed
// error.
// Like every mutation method, it will resolve relations and trigger handlers.
func (m *Machine) AddErr(err error) Result {
+ if m.Disposed {
+ return Canceled
+ }
// TODO test .Err
m.Err = err
+
return m.Add(S{"Exception"}, A{"err": err})
}
@@ -437,39 +852,55 @@ func (m *Machine) IsErr() bool {
// the transition (Executed, Queued, Canceled).
// Like every mutation method, it will resolve relations and trigger handlers.
func (m *Machine) Remove(states S, args A) Result {
+ if m.Disposed || m.queueLen >= m.QueueLimit {
+ return Canceled
+ }
+
// return early if none of the states is active
m.queueLock.RLock()
- lenQueue := len(m.Queue)
+ lenQueue := len(m.queue)
+
// try ignoring this mutation, if none of the states is currently active
var statesAny []S
for _, name := range states {
statesAny = append(statesAny, S{name})
}
+
if lenQueue == 0 && !m.DuringTransition() && !m.Any(statesAny...) {
m.queueLock.RUnlock()
return Executed
}
+
m.queueLock.RUnlock()
- m.queueMutation(MutationTypeRemove, states, args)
+ m.queueMutation(MutationRemove, states, args)
+
return m.processQueue()
}
+// Remove1 is a shorthand method to remove a single state with the passed args.
+// See Remove().
+func (m *Machine) Remove1(state string, args A) Result {
+ return m.Remove(S{state}, args)
+}
+
// Set de-activates a list of states in the machine, returning the result of
// the transition (Executed, Queued, Canceled).
// Like every mutation method, it will resolve relations and trigger handlers.
func (m *Machine) Set(states S, args A) Result {
- m.queueMutation(MutationTypeSet, states, args)
+ if m.Disposed || m.queueLen >= m.QueueLimit {
+ return Canceled
+ }
+ m.queueMutation(MutationSet, states, args)
+
return m.processQueue()
}
// Is checks if all the passed states are currently active.
//
-// ```
-// machine.StringAll() // ()[Foo:0 Bar:0 Baz:0]
-// machine.Add(S{"Foo"})
-// machine.Is(S{"Foo"}) // true
-// machine.Is(S{"Foo", "Bar"}) // false
-// ```
+// machine.StringAll() // ()[Foo:0 Bar:0 Baz:0]
+// machine.Add(S{"Foo"})
+// machine.Is(S{"Foo"}) // true
+// machine.Is(S{"Foo", "Bar"}) // false
func (m *Machine) Is(states S) bool {
m.activeStatesLock.RLock()
defer m.activeStatesLock.RUnlock()
@@ -477,54 +908,91 @@ func (m *Machine) Is(states S) bool {
return m.is(states)
}
-// thread-unsafe version of Is(), make sure to acquire m.activeStatesLock
+// Is1 is a shorthand method to check if a single state is currently active.
+// See Is().
+func (m *Machine) Is1(state string) bool {
+ return m.Is(S{state})
+}
+
+// is is an unsafe version of Is(), make sure to acquire m.activeStatesLock
func (m *Machine) is(states S) bool {
- return lo.Every(m.ActiveStates, m.MustParseStates(states))
+ for _, s := range states {
+ if !slices.Contains(m.activeStates, s) {
+ return false
+ }
+ }
+
+ return true
}
-// Not checks if none of the passed states are currently active.
+// Not checks if **none** of the passed states are currently active.
+//
+// machine.StringAll()
+// // -> ()[A:0 B:0 C:0 D:0]
+// machine.Add(S{"A", "B"})
+//
+// // not(A) and not(C)
+// machine.Not(S{"A", "C"})
+// // -> false
//
-// ```
-// machine.StringAll() // ()[A:0 B:0 C:0 D:0]
-// machine.Add(S{"A", "B"})
-// // not(A) and not(C)
-// machine.Not(S{"A", "C"}) // false
-// // not(C) and not(D)
-// machine.Not(S{"C", "D"}) // true
-// ```
+// // not(C) and not(D)
+// machine.Not(S{"C", "D"})
+// // -> true
func (m *Machine) Not(states S) bool {
m.activeStatesLock.RLock()
defer m.activeStatesLock.RUnlock()
- return lo.None(m.MustParseStates(states), m.ActiveStates)
+ return slicesNone(m.MustParseStates(states), m.activeStates)
+}
+
+// Not1 is a shorthand method to check if a single state is currently inactive.
+// See Not().
+func (m *Machine) Not1(state string) bool {
+ m.activeStatesLock.RLock()
+ defer m.activeStatesLock.RUnlock()
+
+ return slicesNone(m.MustParseStates(S{state}), m.activeStates)
}
-// Any is group call to `Is`, returns true if any of the params return true
+// Any is group call to Is, returns true if any of the params return true
// from Is.
//
-// ```
-// machine.StringAll() // ()[Foo:0 Bar:0 Baz:0]
-// machine.Add(S{"Foo"})
-// // is(Foo, Bar) or is(Bar)
-// machine.Any(S{"Foo", "Bar"}, S{"Bar"}) // false
-// // is(Foo) or is(Bar)
-// machine.Any(S{"Foo"}, S{"Bar"}) // true
-// ```
+// machine.StringAll() // ()[Foo:0 Bar:0 Baz:0]
+// machine.Add(S{"Foo"})
+// // is(Foo, Bar) or is(Bar)
+// machine.Any(S{"Foo", "Bar"}, S{"Bar"}) // false
+// // is(Foo) or is(Bar)
+// machine.Any(S{"Foo"}, S{"Bar"}) // true
func (m *Machine) Any(states ...S) bool {
- return lo.SomeBy(states, func(item S) bool {
- return m.Is(item)
- })
+ for _, s := range states {
+ if m.Is(s) {
+ return true
+ }
+ }
+ return false
+}
+
+// Any1 is group call to Is1(), returns true if any of the params return true
+// from Is1().
+func (m *Machine) Any1(states ...string) bool {
+ for _, s := range states {
+ if m.Is1(s) {
+ return true
+ }
+ }
+ return false
}
// IsClock checks if the passed state's clock equals the passed tick.
//
-// ```
-// tick := m.Clock("A")
-// m.Remove("A")
-// m.Add("A")
-// m.Is("A", tick) // -> false
-// ```
+// tick := m.Clock("A")
+// m.Remove("A")
+// m.Add("A")
+// m.Is("A", tick) // -> false
func (m *Machine) IsClock(state string, tick uint64) bool {
+ m.activeStatesLock.RLock()
+ defer m.activeStatesLock.RUnlock()
+
return m.clock[state] == tick
}
@@ -542,24 +1010,102 @@ func (m *Machine) queueMutation(mutationType MutationType, states S, args A) {
m.log(LogOps, "[queue:%s] %s", mutationType, j(statesParsed))
}
+ // args should always be initialized
+ if args == nil {
+ args = A{}
+ }
+
m.queueLock.Lock()
- m.Queue = append(m.Queue, Mutation{
+ m.queue = append(m.queue, &Mutation{
Type: mutationType,
CalledStates: statesParsed,
Args: args,
Auto: false,
})
+ m.queueLen = len(m.queue)
m.queueLock.Unlock()
}
-// GetStateCtx returns a context, bound to the current clock tick of the
-// passed state.
+// Eval executes a function on the machine's queue, allowing to avoid using
+// locks for non-handler code. Blocking code should NOT be scheduled here.
+// Eval cannot be called within a handler's critical section, as both are using
+// the same serial queue and will deadlock. Eval has a timeout of
+// HandlerTimeout/2 and will return false in case it happens.
+//
+// ctx: nil context defaults to machine's context.
+//
+// Note: usage of Eval is discouraged.
+func (m *Machine) Eval(ctx context.Context, fn func(), source string) bool {
+ if source == "" {
+ panic("Error: source of eval is required")
+ }
+ m.log(LogOps, "[eval] %s", source)
+
+ // wrap the func with a done channel
+ done := make(chan struct{})
+ canceled := false
+ wrap := func() {
+ defer close(done)
+ if canceled {
+ return
+ }
+ fn()
+ }
+
+ if ctx == nil {
+ ctx = context.Background()
+ }
+
+ // TODO detect when called from a handler, skip the queue and avoid deadlocks
+ m.queueLock.Lock()
+
+ // prepend to the queue
+ m.queue = append([]*Mutation{{
+ Type: MutationEval,
+ Eval: wrap,
+ Ctx: ctx,
+ }}, m.queue...)
+ m.queueLen = len(m.queue)
+ m.queueLock.Unlock()
+
+ // process the queue if needed, but ignore the result
+ m.processQueue()
+
+ // wait with a timeout
+ select {
+
+ case <-time.After(m.HandlerTimeout / 2):
+ canceled = true
+ m.log(LogOps, "[eval:timeout] %s", source[0])
+ return false
+
+ case <-m.Ctx.Done():
+ return false
+
+ case <-ctx.Done():
+ m.log(LogDecisions, "[eval:ctxdone] %s", source[0])
+ return false
+
+ case <-done:
+ }
+
+ m.log(LogEverything, "[eval:end] %s", source)
+ return true
+}
+
+// NewStateCtx returns a new sub-context, bound to the current clock's tick of
+// the passed state.
//
// Context cancels when the state has been de-activated, or right away,
// if it isn't currently active.
-func (m *Machine) GetStateCtx(state string) context.Context {
+//
+// State contexts are used to check state expirations and should be checked
+// often inside goroutines.
+// TODO reuse existing ctxs
+func (m *Machine) NewStateCtx(state string) context.Context {
// TODO handle cancellation while parsing the queue
stateCtx, cancel := context.WithCancel(m.Ctx)
+
// close early
if !m.Is(S{state}) {
cancel()
@@ -575,6 +1121,7 @@ func (m *Machine) GetStateCtx(state string) context.Context {
} else {
m.indexStateCtx[state] = append(m.indexStateCtx[state], cancel)
}
+
return stateCtx
}
@@ -585,34 +1132,48 @@ func (m *Machine) SetLogLevel(level LogLevel) {
// BindHandlers binds a struct of handler methods to the machine's states.
// Returns a HandlerBinding object, which signals when the binding is ready.
+// TODO verify method signatures early
+// TODO detect handlers for unknown states
func (m *Machine) BindHandlers(handlers any) error {
- // binding := &HandlerBinding{
- // Ready: make(chan struct{}),
- // }
+ if !m.handlerLoopRunning {
+ m.handlerLoopRunning = true
+
+ // start the handler loop
+ go m.handlerLoop()
+ }
+
v := reflect.ValueOf(handlers)
if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct {
fmt.Println("Function expects a pointer to a struct")
return errors.New("BindHandlers expects a pointer to a struct")
}
+
+ // register the new emitter
name := reflect.TypeOf(handlers).Elem().Name()
- // register a new emitter
m.newEmitter(name, &v)
+
return nil
}
-// On returns a channel that will be notified with *Event, when any of the
-// passed events happen. It's quick substitute for a predefined transition
-// handler, although it does not guarantee a deterministic order of execution.
-// ctx: optional context to dispose the emitter earlier.
-// It's not supported to nest On() calls, as it would cause a deadlock.
-func (m *Machine) On(events []string, ctx context.Context) chan *Event {
- ch := make(chan *Event)
+// OnEvent returns a channel that will be notified with *Event, when any of
+// the passed events happen. It's a quick substitute for predefined transition
+// handlers, although it does not support negotiation.
+//
+// ctx: optional context to dispose the emitter early.
+//
+// It's not supported to nest OnEvent() calls, as it would cause a deadlock.
+// Using OnEvent is recommended only in special cases, like test assertions.
+func (m *Machine) OnEvent(events []string, ctx context.Context) chan *Event {
+ ch := make(chan *Event, 50)
+ if m.Disposed {
+ ch := make(chan *Event)
+ close(ch)
+ return ch
+ }
+
m.indexEventChLock.Lock()
defer m.indexEventChLock.Unlock()
- if ctx == nil {
- ctx = m.Ctx
- }
for _, e := range events {
if _, ok := m.indexEventCh[e]; !ok {
m.indexEventCh[e] = []chan *Event{ch}
@@ -620,28 +1181,39 @@ func (m *Machine) On(events []string, ctx context.Context) chan *Event {
m.indexEventCh[e] = append(m.indexEventCh[e], ch)
}
}
+
// dispose with context
if ctx != nil {
go func() {
- <-ctx.Done()
+ select {
+ case <-ch:
+ return
+ case <-m.Ctx.Done():
+ return
+ case <-ctx.Done():
+ }
+
// GC only if needed
- if m.disposed {
+ if m.Disposed {
return
}
+
m.indexEventChLock.Lock()
for _, e := range events {
if _, ok := m.indexEventCh[e]; ok {
if len(m.indexEventCh[e]) == 1 {
- // delete the whole map, as there's many possible events
+ // delete the whole map, it's the last one
delete(m.indexEventCh, e)
} else {
- m.indexEventCh[e] = lo.Without(m.indexEventCh[e], ch)
+ m.indexEventCh[e] = slicesWithout(m.indexEventCh[e], ch)
}
}
}
+
m.indexEventChLock.Unlock()
}()
}
+
return ch
}
@@ -650,57 +1222,69 @@ func (m *Machine) recoverToErr(emitter *emitter, r any) {
if m.Ctx.Err() != nil {
return
}
+
m.panicCaught = true
+ m.currentHandler = ""
t := m.Transition
+
// dont double handle an exception (no nesting)
- if lo.Contains(t.Mutation.CalledStates, "Exception") {
+ if slices.Contains(t.Mutation.CalledStates, "Exception") {
return
}
+
m.log(LogOps, "[recover] handling panic...")
err, ok := r.(error)
if !ok {
- err = errors.New(fmt.Sprint(r))
+ err = fmt.Errorf("%s", r)
}
m.Err = err
+
// final phase, trouble...
if t.latestStep != nil && t.latestStep.IsFinal {
+
// try to fix active states
finals := S{}
finals = append(finals, t.Exits...)
finals = append(finals, t.Enters...)
m.activeStatesLock.RLock()
- activeStates := m.ActiveStates
+ activeStates := m.activeStates
m.activeStatesLock.RUnlock()
found := false
+
// walk over enter/exits and remove states after the last step,
// as their final handlers haven't been executed
for _, s := range finals {
+
if t.latestStep.FromState == s {
found = true
}
if !found {
continue
}
+
if t.latestStep.IsEnter {
- activeStates = lo.Without(activeStates, s)
+ activeStates = slicesWithout(activeStates, s)
} else {
activeStates = append(activeStates, s)
}
}
+
m.log(LogOps, "[recover] partial final states as (%s)",
j(activeStates))
m.setActiveStates(t.CalledStates(), activeStates, t.IsAuto())
t.IsCompleted = true
}
+
m.log(LogOps, "[cancel] (%s) by recover", j(t.TargetStates))
if t.Mutation == nil {
// TODO can this even happen?
panic(fmt.Sprintf("no mutation panic in %s: %s", emitter.id, err))
}
+
// negotiation phase, simply cancel and...
- // unshift add:Exception to the beginning of the queue
- exception := Mutation{
- Type: MutationTypeAdd,
+ // prepend add:Exception to the beginning of the queue
+ exception := &Mutation{
+ Type: MutationAdd,
CalledStates: S{"Exception"},
Args: A{
"err": err,
@@ -713,39 +1297,64 @@ func (m *Machine) recoverToErr(emitter *emitter, r any) {
},
},
}
- m.Queue = append([]Mutation{exception}, m.Queue...)
+
+ m.queue = append([]*Mutation{exception}, m.queue...)
+ // restart the handler loop
+ go m.handlerLoop()
}
// MustParseStates parses the states and returns them as a list.
-// Panics when a state is not defined.
+// Panics when a state is not defined. It's an usafe equivalent of VerifyStates.
func (m *Machine) MustParseStates(states S) S {
- // check if all states are defined in m.States
+ // check if all states are defined in m.Struct
for _, s := range states {
- if _, ok := m.States[s]; !ok {
+ if _, ok := m.states[s]; !ok {
panic(fmt.Sprintf("state %s is not defined", s))
}
}
- return lo.Uniq(states)
+
+ return slicesUniq(states)
}
// VerifyStates verifies an array of state names and returns an error in case
// at least one isn't defined. It also retains the order and uses it for
-// StateNames (only if all states have been passed).
+// StateNames (only if all states have been passed). Not thread-safe.
func (m *Machine) VerifyStates(states S) error {
var errs []error
+ var checked []string
+
for _, s := range states {
- if _, ok := m.States[s]; !ok {
+
+ if slices.Contains(checked, s) {
+ errs = append(errs, fmt.Errorf("state %s duplicated", s))
+ continue
+ }
+
+ if _, ok := m.states[s]; !ok {
errs = append(errs, fmt.Errorf("state %s is not defined", s))
+ continue
}
+
+ // TODO verify references, unknown fields
+
+ checked = append(checked, s)
}
+
if len(errs) > 1 {
return errors.Join(errs...)
} else if len(errs) == 1 {
return errs[0]
}
- if len(states) == len(m.StateNames) {
- m.StateNames = states
+
+ if len(m.StateNames) > len(states) {
+ missing := DiffStates(m.StateNames, checked)
+ return fmt.Errorf("undefined states: %s", j(missing))
}
+
+ // memorize the state names order
+ m.StateNames = slices.Clone(states)
+ m.StatesVerified = true
+
return nil
}
@@ -757,24 +1366,35 @@ func (m *Machine) setActiveStates(calledStates S, targetStates S,
m.activeStatesLock.Lock()
defer m.activeStatesLock.Unlock()
- previous := m.ActiveStates
- newStates := DiffStates(targetStates, m.ActiveStates)
- removedStates := DiffStates(m.ActiveStates, targetStates)
+ previous := m.activeStates
+ newStates := DiffStates(targetStates, m.activeStates)
+ removedStates := DiffStates(m.activeStates, targetStates)
noChangeStates := DiffStates(targetStates, newStates)
- m.ActiveStates = targetStates
- // Tick all new states and called multi states
+ m.activeStates = slices.Clone(targetStates)
+
+ // Tick all new states by +1 and already active and called multi states by +2
for _, state := range targetStates {
- data := m.States[state]
- if !lo.Contains(previous, state) ||
- (lo.Contains(calledStates, state) && data.Multi) {
+
+ data := m.states[state]
+ if !slices.Contains(previous, state) {
+ // tick by +1
+ // TODO wrap on overflow
m.clock[state]++
- // treat multi states as new states (for logging)
- if data.Multi && lo.Contains(previous, state) {
- newStates = append(newStates, state)
- }
+ } else if slices.Contains(calledStates, state) && data.Multi {
+
+ // tick by +2 to indicate a new instance
+ // TODO wrap on overflow
+ m.clock[state] += 2
+ // treat prev active multi states as new states, for logging
+ newStates = append(newStates, state)
}
}
+ // tick de-activated states by +1
+ for _, state := range removedStates {
+ m.clock[state]++
+ }
+
// construct a logging msg
logMsg := ""
if len(newStates) > 0 {
@@ -786,12 +1406,15 @@ func (m *Machine) setActiveStates(calledStates S, targetStates S,
if len(noChangeStates) > 0 && m.logLevel > LogDecisions {
logMsg += " " + j(noChangeStates)
}
+
if len(logMsg) > 0 {
autoLabel := ""
if isAuto {
autoLabel = ":auto"
}
- m.log(LogChanges, "[state%s]"+logMsg, autoLabel)
+
+ args := m.Transition.LogArgs()
+ m.log(LogChanges, "[state%s]"+logMsg+args, autoLabel)
}
return previous
@@ -800,46 +1423,73 @@ func (m *Machine) setActiveStates(calledStates S, targetStates S,
// processQueue processes the queue of mutations. It's the main loop of the
// machine.
func (m *Machine) processQueue() Result {
- m.queueLock.RLock()
- lenQueue := len(m.Queue)
- m.queueLock.RUnlock()
// empty queue
- if lenQueue == 0 {
+ if len(m.queue) == 0 || m.Disposed {
return Canceled
}
- // acquire the atomic lock
- if !m.queueProcessing.CompareAndSwap(false, true) {
+
+ // try to acquire the lock
+ acquired := m.queueProcessing.TryLock()
+ if !acquired {
+
+ m.queueLock.Lock()
+ defer m.queueLock.Unlock()
+
label := "items"
- if lenQueue == 1 {
+ if len(m.queue) == 1 {
label = "item"
}
- m.log(LogOps, "[postpone] queue running (%d %s)", lenQueue, label)
+ m.log(LogOps, "[postpone] queue running (%d %s)", len(m.queue), label)
+ m.emit(EventQueueAdd, A{"queue_len": len(m.queue)}, nil)
+
return Queued
}
var ret []Result
// execute the queue
- for lenQueue > 0 {
+ for len(m.queue) > 0 {
+
+ if m.Disposed {
+ return Canceled
+ }
+ if len(m.queue) == 0 {
+ break
+ }
+
// shift the queue
m.queueLock.Lock()
- item := &m.Queue[0]
- m.Queue = m.Queue[1:]
+ item := m.queue[0]
+ m.queue = m.queue[1:]
+ m.queueLen = len(m.queue)
m.queueLock.Unlock()
+
+ // support for context cancellation
+ if item.Ctx != nil && item.Ctx.Err() != nil {
+ ret = append(ret, Executed)
+ continue
+ }
+
+ // special case for Eval mutations
+ if item.Type == MutationEval {
+ item.Eval()
+ continue
+ }
newTransition(m, item)
+
// execute the transition
ret = append(ret, m.Transition.emitEvents())
+
+ // process flow methods
m.processWhenBindings()
+ m.processWhenTimeBindings()
m.processStateCtxBindings()
- m.queueLock.RLock()
- lenQueue = len(m.Queue)
- m.queueLock.RUnlock()
}
- m.emit(EventQueueEnd, nil, nil)
- m.Transition = nil
// release the atomic lock
- m.queueProcessing.Swap(false)
+ m.Transition = nil
+ m.queueProcessing.Unlock()
+ m.emit(EventQueueEnd, nil, nil)
if len(ret) == 0 {
return Canceled
@@ -849,14 +1499,17 @@ func (m *Machine) processQueue() Result {
func (m *Machine) processStateCtxBindings() {
m.activeStatesLock.RLock()
+ deactivated := DiffStates(m.Transition.StatesBefore, m.activeStates)
- deactivated := DiffStates(m.Transition.StatesBefore, m.ActiveStates)
var toCancel []context.CancelFunc
for _, s := range deactivated {
+
toCancel = append(toCancel, m.indexStateCtx[s]...)
delete(m.indexStateCtx, s)
}
+
m.activeStatesLock.RUnlock()
+
// cancel all the state contexts outside the critical zone
for _, cancel := range toCancel {
cancel()
@@ -865,61 +1518,141 @@ func (m *Machine) processStateCtxBindings() {
func (m *Machine) processWhenBindings() {
m.activeStatesLock.Lock()
- activated := DiffStates(m.ActiveStates, m.Transition.StatesBefore)
- deactivated := DiffStates(m.Transition.StatesBefore, m.ActiveStates)
+
+ // calculate activated and deactivated states
+ activated := DiffStates(m.activeStates, m.Transition.StatesBefore)
+ deactivated := DiffStates(m.Transition.StatesBefore, m.activeStates)
+
+ // merge all states
all := S{}
all = append(all, activated...)
all = append(all, deactivated...)
+
var toClose []chan struct{}
for _, s := range all {
for k, binding := range m.indexWhen[s] {
- if lo.Contains(activated, s) {
- // activated
+
+ if slices.Contains(activated, s) {
+
+ // state activated, check the index
if !binding.negation {
- // When(
+ // match for When(
if !binding.states[s] {
binding.matched++
}
} else {
- // WhenNot(
+ // match for WhenNot(
if !binding.states[s] {
binding.matched--
}
}
- // mark as active
+
+ // update index: mark as active
binding.states[s] = true
} else {
- // deactivated
+
+ // state deactivated
if !binding.negation {
- // When(
+ // match for When(
if binding.states[s] {
binding.matched--
}
} else {
- // WhenNot(
+ // match for WhenNot(
if binding.states[s] {
binding.matched++
}
}
- // mark as inactive
+
+ // update index: mark as inactive
binding.states[s] = false
}
+
+ // if not all matched, ignore for now
if binding.matched < binding.total {
continue
}
+
// completed - close and delete indexes for all involved states
+ var names []string
for state := range binding.states {
+ names = append(names, state)
+
if len(m.indexWhen[state]) == 1 {
delete(m.indexWhen, state)
continue
}
+
if state == s {
m.indexWhen[s] = append(m.indexWhen[s][:k], m.indexWhen[s][k+1:]...)
continue
}
- // TODO slow?
- m.indexWhen[state] = lo.Without(m.indexWhen[state], binding)
+
+ m.indexWhen[state] = slices.Delete(m.indexWhen[state], k, k+1)
+ }
+
+ m.log(LogDecisions, "[when] match for (%s)", j(names))
+ // close outside the critical zone
+ toClose = append(toClose, binding.ch)
+ }
+ }
+ m.activeStatesLock.Unlock()
+
+ // notify outside the critical zone
+ for ch := range toClose {
+ closeSafe(toClose[ch])
+ }
+}
+
+func (m *Machine) processWhenTimeBindings() {
+ m.activeStatesLock.Lock()
+ indexWhenTime := m.indexWhenTime
+ var toClose []chan struct{}
+
+ // collect all the ticked states
+ all := S{}
+ for state, t := range m.Transition.ClocksBefore {
+ // if changed, collect to check
+ if m.clock[state] != t {
+ all = append(all, state)
+ }
+ }
+
+ // check all the bindings for all the ticked states
+ for _, s := range all {
+ for k, binding := range indexWhenTime[s] {
+
+ // check if the requested time has passed
+ if !binding.completed[s] &&
+ m.clock[s] >= binding.times[binding.index[s]] {
+ binding.matched++
+ // mark in the index as completed
+ binding.completed[s] = true
+ }
+
+ // if not all matched, ignore for now
+ if binding.matched < binding.total {
+ continue
+ }
+
+ // completed - close and delete indexes for all involved states
+ var names []string
+ for state := range binding.index {
+ names = append(names, state)
+ if len(indexWhenTime[state]) == 1 {
+ delete(indexWhenTime, state)
+ continue
+ }
+ if state == s {
+ indexWhenTime[s] = append(indexWhenTime[s][:k],
+ indexWhenTime[s][k+1:]...)
+ continue
+ }
+
+ indexWhenTime[state] = slices.Delete(indexWhenTime[state], k, k+1)
}
+
+ m.log(LogDecisions, "[when:time] match for (%s)", j(names))
// close outside the critical zone
toClose = append(toClose, binding.ch)
}
@@ -931,10 +1664,32 @@ func (m *Machine) processWhenBindings() {
}
}
-// Log logs an [external] message with the LogChanges level (highest one).
+// SetLogArgs accepts a function which decides which mutation arguments to log.
+// See NewArgsMapper or create your own manually.
+func (m *Machine) SetLogArgs(matcher func(args A) map[string]string) {
+ m.logArgs = matcher
+}
+
+// GetLogArgs returns the current log args function.
+func (m *Machine) GetLogArgs() func(args A) map[string]string {
+ return m.logArgs
+}
+
+// Log logs an [extern] message unless LogNothing is set (default).
// Optionally redirects to a custom logger from SetLogger.
func (m *Machine) Log(msg string, args ...any) {
- m.log(LogChanges, "[external] "+msg, args...)
+ prefix := "[extern"
+
+ // try to prefix with the current handler, if present
+ handler := m.currentHandler
+ if handler != "" {
+ prefix += ":" + truncateStr(handler, 15)
+ }
+
+ // single lines only
+ msg = strings.ReplaceAll(msg, "\n", " ")
+
+ m.log(LogChanges, prefix+"] "+msg, args...)
}
// log logs a message if the log level is high enough.
@@ -943,9 +1698,15 @@ func (m *Machine) log(level LogLevel, msg string, args ...any) {
if level > m.logLevel || m.Ctx.Err() != nil {
return
}
+
if m.LogID {
- msg = "[" + m.ID[:5] + "] " + msg
+ id := m.ID
+ if len(id) > 5 {
+ id = id[:5]
+ }
+ msg = "[" + id + "] " + msg
}
+
out := fmt.Sprintf(msg, args...)
if m.logger != nil {
m.logger(level, msg, args...)
@@ -956,8 +1717,10 @@ func (m *Machine) log(level LogLevel, msg string, args ...any) {
t := m.Transition
if t != nil {
// append the log msg to the current transition
+ // TODO not thread safe
t.LogEntries = append(t.LogEntries, out)
} else {
+
// append the log msg the machine and collect at the end of the next
// transition
m.logEntriesLock.Lock()
@@ -971,6 +1734,20 @@ func (m *Machine) SetLogger(fn Logger) {
m.logger = fn
}
+// SetTestLogger hooks into the testing.Logf and set the log level in one call.
+// Useful for testing. Requires LogChanges log level to produce any output.
+func (m *Machine) SetTestLogger(
+ logf func(format string, args ...any), level LogLevel,
+) {
+ if logf == nil {
+ panic("logf cannot be nil")
+ }
+ m.logger = func(_ LogLevel, msg string, args ...any) {
+ logf(msg, args...)
+ }
+ m.logLevel = level
+}
+
// GetLogger returns the current custom logger function, or nil.
func (m *Machine) GetLogger() Logger {
return m.logger
@@ -980,7 +1757,7 @@ func (m *Machine) GetLogger() Logger {
// Can block indefinitely if the handler doesn't return or the emitter isn't
// accepting events.
func (m *Machine) emit(
- name string, args A, step *TransitionStep,
+ name string, args A, step *Step,
) (Result, bool) {
e := &Event{
Name: name,
@@ -988,22 +1765,25 @@ func (m *Machine) emit(
Args: args,
step: step,
}
- t := m.Transition
+
// queue-end lacks a transition
targetStates := "---"
- if t != nil {
- targetStates = j(t.TargetStates)
+ if m.Transition != nil && m.Transition.TargetStates != nil {
+ targetStates = j(m.Transition.TargetStates)
}
+
// call the handlers
res, handlerCalled := m.processEmitters(e)
if m.panicCaught {
res = Canceled
m.panicCaught = false
}
+
// check if this is an internal event
if step == nil {
return Executed, handlerCalled
}
+
// negotiation support
if !step.IsFinal && res == Canceled {
var self string
@@ -1014,6 +1794,7 @@ func (m *Machine) emit(
targetStates, name)
return Canceled, handlerCalled
}
+
return Executed, handlerCalled
}
@@ -1021,65 +1802,75 @@ func (m *Machine) processEmitters(e *Event) (Result, bool) {
var emitter *emitter
handlerCalled := false
for _, emitter = range m.emitters {
- if m.Ctx.Err() != nil {
+ // disposed
+ if m.Disposed {
break
}
- method := e.Name
// internal event
if e.step == nil {
break
}
- if e.step != nil {
- emitterID := emitter.id
- if len(emitterID) > 15 {
- emitterID = emitterID[:15]
- }
- emitterID = padString(strings.ReplaceAll(emitterID, " ", "_"), 15, "_")
- m.log(LogEverything, "[emit:%-15s] %s", emitterID, method)
- }
- // if no handler, skip
+
+ method := e.Name
+
+ // format a log msg
+ emitterID := truncateStr(emitter.id, 15)
+ emitterID = padString(strings.ReplaceAll(emitterID, " ", "_"), 15, "_")
+ // TODO confirm this is useful
+ m.log(LogEverything, "[emit:%-15s] %s", emitterID, method)
+
+ // TODO cache
if !emitter.methods.MethodByName(method).IsValid() {
continue
}
- m.log(LogOps, "[handler] %s", method)
+
// call the handler
+ m.log(LogOps, "[handler] %s", method)
+ m.currentHandler = method
var ret bool
handlerCalled = true
+ // tracers
+ for i := range m.Tracers {
+ m.Tracers[i].HandlerStart(m.Transition, emitter.id, method)
+ }
+ handler := &handlerCall{
+ fn: emitter.methods.MethodByName(e.Name),
+ event: e,
+ timeout: false,
+ }
+ m.handlerTimer.Reset(m.HandlerTimeout)
+ m.handlerStart <- handler
- go func() {
- if m.PanicToException {
- // catch panics and fwd
- defer func() {
- r := recover()
- if r != nil {
- m.handlerPanic <- r
- }
- }()
- }
- callRet := emitter.methods.MethodByName(e.Name).Call(
- []reflect.Value{reflect.ValueOf(e)})
- if len(callRet) > 0 {
- m.handlerDone <- callRet[0].Interface().(bool)
- return
- }
- // handlers return true by default
- m.handlerDone <- true
- }()
// wait on the result / timeout / context
select {
+
case <-m.Ctx.Done():
break
- case <-time.After(m.HandlerTimeout):
+
+ case <-m.handlerTimer.C:
m.log(LogOps, "[cancel] (%s) by timeout",
j(m.Transition.TargetStates))
break
- case ret = <-m.handlerDone:
- // ok
+
case r := <-m.handlerPanic:
// recover partial state
m.recoverToErr(emitter, r)
ret = false
+
+ case ret = <-m.handlerEnd:
+ m.log(LogEverything, "[handler:end] %s", e.Name)
+ // ok
}
+
+ m.handlerTimer.Stop()
+ m.currentHandler = ""
+
+ // tracers
+ for i := range m.Tracers {
+ m.Tracers[i].HandlerEnd(m.Transition, emitter.id, method)
+ }
+
+ // handle negotiation
switch {
case strings.HasSuffix(e.Name, "State"):
case strings.HasSuffix(e.Name, "End"):
@@ -1090,20 +1881,99 @@ func (m *Machine) processEmitters(e *Event) (Result, bool) {
}
}
}
- if m.processEventChs(e) == Canceled {
- return Canceled, handlerCalled
- }
+
+ // dynamic handlers
+ m.processEventChs(e)
+ // state args matchers
+ m.processWhenArgs(e)
+
return Executed, handlerCalled
}
-// processEventChs sends the event to all On() dynamic handlers.
-func (m *Machine) processEventChs(e *Event) Result {
+func (m *Machine) handlerLoop() {
+ if m.PanicToException {
+ // catch panics and fwd
+ defer func() {
+ r := recover()
+ if r != nil && !m.Disposed {
+ m.handlerPanic <- r
+ }
+ }()
+ }
+
+ // wait on the result / timeout / context
+ for {
+ select {
+
+ case <-m.Ctx.Done():
+ // dispose with context
+ m.Dispose()
+ return
+
+ case call, ok := <-m.handlerStart:
+ if !ok {
+ return
+ }
+ ret := true
+
+ // handler signature: FooState(e *am.Event)
+ callRet := call.fn.Call([]reflect.Value{reflect.ValueOf(call.event)})
+ if len(callRet) > 0 {
+ ret = callRet[0].Interface().(bool)
+ }
+
+ if call.timeout || m.Disposed {
+ continue
+ }
+
+ m.handlerEnd <- ret
+ }
+ }
+}
+
+// processEventChs sends the event to all OnEvent() dynamic handlers.
+func (m *Machine) processEventChs(e *Event) {
m.indexEventChLock.Lock()
defer m.indexEventChLock.Unlock()
+
for _, ch := range m.indexEventCh[e.Name] {
- ch <- e
+ select {
+
+ case ch <- e:
+
+ case <-time.After(m.HandlerTimeout):
+ }
+ }
+}
+
+func (m *Machine) processWhenArgs(e *Event) {
+ // check if a final entry handler (FooState)
+ if e.step == nil || !e.step.IsFinal || !e.step.IsEnter {
+ return
+ }
+
+ // process args channels
+ m.indexWhenArgsLock.Lock()
+ var chToClose []chan struct{}
+ for _, binding := range m.indexWhenArgs[e.Name] {
+ if !compareArgs(e.Args, binding.args) {
+ continue
+ }
+ m.log(LogDecisions, "[when:args] match for %s", e.Name)
+ // args match - dispose and close outside the mutex
+ chToClose = append(chToClose, binding.ch)
+ // GC
+ if len(m.indexWhenArgs[e.Name]) == 1 {
+ delete(m.indexWhenArgs, e.Name)
+ } else {
+ m.indexWhenArgs[e.Name] = slicesWithout(m.indexWhenArgs[e.Name], binding)
+ }
+ }
+ m.indexWhenArgsLock.Unlock()
+
+ for _, ch := range chToClose {
+ closeSafe(ch)
}
- return Executed
}
// detectQueueDuplicates checks for duplicated mutations without params.
@@ -1119,14 +1989,14 @@ func (m *Machine) detectQueueDuplicates(mutationType MutationType,
}
var counterMutationType MutationType
switch mutationType {
- case MutationTypeAdd:
- counterMutationType = MutationTypeRemove
- case MutationTypeRemove:
- counterMutationType = MutationTypeAdd
+ case MutationAdd:
+ counterMutationType = MutationRemove
+ case MutationRemove:
+ counterMutationType = MutationAdd
default:
- case MutationTypeSet:
+ case MutationSet:
// avoid duplicating `set` only if at the end of the queue
- return index > 0 && len(m.Queue)-1 > 0
+ return index > 0 && len(m.queue)-1 > 0
}
// Check if a counter mutation is scheduled and broaden the match
// - with or without params
@@ -1147,31 +2017,25 @@ func (m *Machine) Clock(state string) uint64 {
return m.clock[state]
}
-// From returns the states from before the transition. Assumes DuringTransition.
-func (m *Machine) From() S {
- return m.Transition.StatesBefore
-}
-
-// To returns the target states of the transition. Assumes DuringTransition.
-func (m *Machine) To() S {
- return m.Transition.TargetStates
-}
-
// IsQueued checks if a particular mutation has been queued. Returns
// an index of the match or -1 if not found.
//
// mutationType: add, remove, set
+//
// states: list of states used in the mutation
+//
// withoutParamsOnly: matches only mutation without the arguments object
+//
// statesStrictEqual: states of the mutation have to be exactly like `states`
// and not a superset.
+// TODO test
func (m *Machine) IsQueued(mutationType MutationType, states S,
withoutArgsOnly bool, statesStrictEqual bool, startIndex int,
) int {
m.queueLock.RLock()
defer m.queueLock.RUnlock()
- for index, item := range m.Queue {
+ for index, item := range m.queue {
if index >= startIndex &&
item.Type == mutationType &&
((withoutArgsOnly && len(item.Args) == 0) || !withoutArgsOnly) &&
@@ -1182,7 +2046,7 @@ func (m *Machine) IsQueued(mutationType MutationType, states S,
(!statesStrictEqual &&
len(item.CalledStates) >= len(states))) &&
// and all the checked ones have to be included in the target ones
- lo.Every(item.CalledStates, states) {
+ slicesEvery(item.CalledStates, states) {
return index
}
}
@@ -1190,42 +2054,55 @@ func (m *Machine) IsQueued(mutationType MutationType, states S,
}
// Has return true is passed states are registered in the machine.
+// TODO test
func (m *Machine) Has(states S) bool {
- return lo.Every(m.StateNames, states)
+ return slicesEvery(m.StateNames, states)
+}
+
+// Has1 is a shorthand for Has. It returns true if the passed state is
+// registered in the machine.
+func (m *Machine) Has1(state string) bool {
+ return m.Has(S{state})
}
// HasStateChanged checks current active states have changed from the passed
// ones.
-// Optionally also compares clock ticks.
-func (m *Machine) HasStateChanged(before S, clocks Clocks) bool {
+func (m *Machine) HasStateChanged(before S) bool {
m.activeStatesLock.RLock()
defer m.activeStatesLock.RUnlock()
- lenEqual := len(before) == len(m.ActiveStates)
- if !lenEqual || len(DiffStates(before, m.ActiveStates)) > 0 {
+ lenEqual := len(before) == len(m.activeStates)
+ if !lenEqual || len(DiffStates(before, m.activeStates)) > 0 {
return true
}
- if clocks == nil {
- return true
- }
- // compare clocks
+
+ return false
+}
+
+// HasStateChangedSince checks if the machine has changed since the passed
+// clocks. Returns true if at least one state has changed.
+func (m *Machine) HasStateChangedSince(clocks Clocks) bool {
+ m.activeStatesLock.RLock()
+ defer m.activeStatesLock.RUnlock()
+
for state, tick := range clocks {
if m.clock[state] != tick {
return true
}
}
+
return false
}
// String returns a one line representation of the currently active states,
// with their clock values. Inactive states are omitted.
-// Eg: (Foo:2 Bar:1)
+// Eg: (Foo:1 Bar:3)
func (m *Machine) String() string {
m.activeStatesLock.RLock()
defer m.activeStatesLock.RUnlock()
ret := "("
- for _, state := range m.ActiveStates {
+ for _, state := range m.activeStates {
if ret != "(" {
ret += " "
}
@@ -1236,7 +2113,7 @@ func (m *Machine) String() string {
// StringAll returns a one line representation of all the states, with their
// clock values. Inactive states are in square brackets.
-// Eg: (Foo:2 Bar:1)[Baz:0]
+// Eg: (Foo:1 Bar:3)[Baz:2]
func (m *Machine) StringAll() string {
m.activeStatesLock.RLock()
defer m.activeStatesLock.RUnlock()
@@ -1244,7 +2121,7 @@ func (m *Machine) StringAll() string {
ret := "("
ret2 := "["
for _, state := range m.StateNames {
- if lo.Contains(m.ActiveStates, state) {
+ if slices.Contains(m.activeStates, state) {
if ret != "(" {
ret += " "
}
@@ -1269,13 +2146,16 @@ func (m *Machine) Inspect(states S) string {
if states == nil {
states = m.StateNames
}
+
ret := ""
for _, name := range states {
- state := m.States[name]
+
+ state := m.states[name]
active := "false"
- if lo.Contains(m.ActiveStates, name) {
+ if slices.Contains(m.activeStates, name) {
active = "true"
}
+
ret += name + ":\n"
ret += fmt.Sprintf(" State: %s %d\n", active, m.clock[name])
if state.Auto {
@@ -1298,20 +2178,164 @@ func (m *Machine) Inspect(states S) string {
}
ret += "\n"
}
+
return ret
}
// Switch returns the first state from the passed list that is currently active,
// making it useful for switch statements.
+//
+// switch mach.Switch(ss.GroupPlaying...) {
+// case "Playing":
+// case "Paused":
+// case "Stopped":
+// }
func (m *Machine) Switch(states ...string) string {
+ activeStates := m.ActiveStates()
+
for _, state := range states {
- if lo.Contains(m.ActiveStates, state) {
+ if slices.Contains(activeStates, state) {
return state
}
}
+
return ""
}
+// GetLogLevel returns the log level of the machine.
func (m *Machine) GetLogLevel() LogLevel {
return m.logLevel
}
+
+// RegisterDisposalHandler adds a function to be called when the machine is
+// disposed. This function will block before mach.WhenDispose is closed.
+func (m *Machine) RegisterDisposalHandler(fn func()) {
+ m.disposeHandlers = append(m.disposeHandlers, fn)
+}
+
+// ActiveStates returns a copy of the currently active states.
+func (m *Machine) ActiveStates() S {
+ m.activeStatesLock.RLock()
+ defer m.activeStatesLock.RUnlock()
+
+ return slices.Clone(m.activeStates)
+}
+
+// Queue returns a copy of the currently active states.
+func (m *Machine) Queue() []*Mutation {
+ m.queueLock.RLock()
+ defer m.queueLock.RUnlock()
+
+ return slices.Clone(m.queue)
+}
+
+// GetStruct returns a copy of machine's state structure.
+func (m *Machine) GetStruct() Struct {
+ m.statesLock.RLock()
+ defer m.statesLock.RUnlock()
+
+ return maps.Clone(m.states)
+}
+
+// SetStruct sets the machine's state structure. It will automatically call
+// VerifyStates with the names param and emit EventStructChange if successful.
+// Note: it's not recommended to change the states structure of a machine which
+// has already produced transitions.
+func (m *Machine) SetStruct(statesStruct Struct, names S) error {
+ m.statesLock.RLock()
+ defer m.statesLock.RUnlock()
+ m.queueLock.RLock()
+ defer m.queueLock.RUnlock()
+
+ old := m.states
+ m.states = parseStruct(statesStruct)
+
+ err := m.VerifyStates(names)
+ if err != nil {
+ return err
+ }
+
+ m.emit(EventStructChange, A{
+ "states_old": old,
+ "states_new": CloneStates(m.states),
+ }, nil)
+
+ return nil
+}
+
+// newEmitter creates a new emitter for Machine.
+// Each emitter should be consumed by one receiver only to guarantee the
+// delivery of all events.
+func (m *Machine) newEmitter(name string, methods *reflect.Value) *emitter {
+ e := &emitter{
+ id: name,
+ methods: methods,
+ }
+ // TODO emitter mutex?
+ m.emitters = append(m.emitters, e)
+
+ return e
+}
+
+// AddFromEv TODO AddFromEv
+// Planned.
+func (m *Machine) AddFromEv(states S, event *Event, args A) Result {
+ panic("AddFromEv not implemented; github.com/pancsta/asyncmachine-go/pulls")
+}
+
+// RemoveFromEv TODO RemoveFromEv
+// Planned.
+func (m *Machine) RemoveFromEv(states S, event *Event, args A) Result {
+ panic(
+ "RemoveFromEv not implemented; github.com/pancsta/asyncmachine-go/pulls")
+}
+
+// SetFromEv TODO SetFromEv
+// Planned.
+func (m *Machine) SetFromEv(states S, event *Event, args A) Result {
+ panic("SetFromEv not implemented; github.com/pancsta/asyncmachine-go/pulls")
+}
+
+// CanAdd TODO CanAdd
+// Planned.
+func (m *Machine) CanAdd(states S) bool {
+ panic("CanAdd not implemented; github.com/pancsta/asyncmachine-go/pulls")
+}
+
+// CanSet TODO CanSet
+// Planned.
+func (m *Machine) CanSet(states S) bool {
+ panic("CanSet not implemented; github.com/pancsta/asyncmachine-go/pulls")
+}
+
+// CanRemove TODO CanRemove
+// Planned.
+func (m *Machine) CanRemove(states S) bool {
+ panic("CanRemove not implemented; github.com/pancsta/asyncmachine-go/pulls")
+}
+
+// WhenQueueEnds writes every time the queue ends with the times from after
+// the last mutation. The channel will close with ctx or Machine.Ctx.
+// TODO WhenQueueEnds
+// Planned.
+func (m *Machine) WhenQueueEnds(ctx context.Context) <-chan T {
+ panic(
+ "WhenQueueEnds not implemented; github.com/pancsta/asyncmachine-go/pulls")
+}
+
+// Clocks returns the map of specified cloks or all clocks if states is nil.
+func (m *Machine) Clocks(states S) Clocks {
+ if states == nil {
+ return maps.Clone(m.clock)
+ }
+
+ m.activeStatesLock.RLock()
+ defer m.activeStatesLock.RUnlock()
+
+ ret := Clocks{}
+ for _, state := range states {
+ ret[state] = m.clock[state]
+ }
+
+ return ret
+}
diff --git a/pkg/machine/machine_test.go b/pkg/machine/machine_test.go
index b38acfd..1d62c02 100644
--- a/pkg/machine/machine_test.go
+++ b/pkg/machine/machine_test.go
@@ -1,4 +1,4 @@
-package machine_test
+package machine
import (
"context"
@@ -8,40 +8,58 @@ import (
"time"
"github.com/stretchr/testify/assert"
-
- am "github.com/pancsta/asyncmachine-go/pkg/machine"
)
-type (
- S = am.S
- A = am.A
- T = am.T
-)
+// TODO ExampleNew
+func ExampleNew() {
+ t := &testing.T{} // Replace this with actual *testing.T in real test cases
+ initialState := S{"A"}
+
+ mach := NewNoRels(t, initialState)
+
+ // Use the machine m here
+ _ = mach
+}
+
+// TODO ExampleNewCommon
+func ExampleNewCommon() {
+ t := &testing.T{} // Replace this with actual *testing.T in real test cases
+ initialState := S{"A"}
+
+ mach := NewNoRels(t, initialState)
+
+ // Use the machine m here
+ _ = mach
+}
// NewNoRels creates a new machine with no relations between states.
-func NewNoRels(t *testing.T, initialState S) *am.Machine {
- m := am.New(context.Background(), am.States{
+func NewNoRels(t *testing.T, initialState S) *Machine {
+ m := New(context.Background(), Struct{
"A": {},
"B": {},
"C": {},
"D": {},
}, nil)
- m.SetLogger(func(i am.LogLevel, msg string, args ...any) {
+
+ m.SetLogger(func(i LogLevel, msg string, args ...any) {
t.Logf(msg, args...)
})
+
if os.Getenv("AM_DEBUG") != "" {
- m.SetLogLevel(am.LogEverything)
+ m.SetLogLevel(LogEverything)
m.HandlerTimeout = 2 * time.Minute
}
+
if initialState != nil {
m.Set(initialState, nil)
}
+
return m
}
// NewRels creates a new machine with basic relations between states.
-func NewRels(t *testing.T, initialState S) *am.Machine {
- m := am.New(context.Background(), am.States{
+func NewRels(t *testing.T, initialState S) *Machine {
+ m := New(context.Background(), Struct{
"A": {
Auto: true,
Require: S{"C"},
@@ -57,11 +75,11 @@ func NewRels(t *testing.T, initialState S) *am.Machine {
Add: S{"C", "B"},
},
}, nil)
- m.SetLogger(func(i am.LogLevel, msg string, args ...any) {
+ m.SetLogger(func(i LogLevel, msg string, args ...any) {
t.Logf(msg, args...)
})
if os.Getenv("AM_DEBUG") != "" {
- m.SetLogLevel(am.LogEverything)
+ m.SetLogLevel(LogEverything)
m.HandlerTimeout = 2 * time.Minute
}
if initialState != nil {
@@ -71,13 +89,13 @@ func NewRels(t *testing.T, initialState S) *am.Machine {
}
// NewNoRels creates a new machine with no relations between states.
-func NewCustomStates(t *testing.T, states am.States) *am.Machine {
- m := am.New(context.Background(), states, nil)
- m.SetLogger(func(i am.LogLevel, msg string, args ...any) {
+func NewCustomStates(t *testing.T, states Struct) *Machine {
+ m := New(context.Background(), states, nil)
+ m.SetLogger(func(i LogLevel, msg string, args ...any) {
t.Logf(msg, args...)
})
if os.Getenv("AM_DEBUG") != "" {
- m.SetLogLevel(am.LogEverything)
+ m.SetLogLevel(LogEverything)
m.HandlerTimeout = 2 * time.Minute
}
return m
@@ -132,7 +150,7 @@ func TestPanicWhenStateIsUnknown(t *testing.T) {
}
func TestGetStateRelations(t *testing.T) {
- m := am.New(context.Background(), am.States{
+ m := New(context.Background(), Struct{
"A": {
Add: S{"B"},
Require: S{"B"},
@@ -141,12 +159,17 @@ func TestGetStateRelations(t *testing.T) {
"B": {},
}, nil)
defer m.Dispose()
- assert.Equal(t, []am.Relation{am.RelationAdd, am.RelationRequire},
- m.GetRelationsOf("A"))
+
+ of, err := m.Resolver.GetRelationsOf("A")
+
+ assert.NoErrorf(t, err, "all states are known")
+ assert.Nil(t, err)
+ assert.Equal(t, []Relation{RelationAdd, RelationRequire},
+ of)
}
func TestGetRelationsBetweenStates(t *testing.T) {
- m := am.New(context.Background(), am.States{
+ m := New(context.Background(), Struct{
"A": {
Add: S{"B"},
Require: S{"C"},
@@ -156,27 +179,37 @@ func TestGetRelationsBetweenStates(t *testing.T) {
"C": {},
}, nil)
defer m.Dispose()
- assert.Equal(t, []am.Relation{am.RelationAdd},
- m.GetRelationsBetween("A", "B"))
+
+ between, err := m.Resolver.GetRelationsBetween("A", "B")
+
+ assert.NoError(t, err)
+ assert.Equal(t, []Relation{RelationAdd},
+ between)
}
func TestSingleToSingleStateTransition(t *testing.T) {
m := NewNoRels(t, S{"A", "B"})
defer m.Dispose()
+
+ // expectations
events := []string{
"AExit", "AC", "AAny", "BExit", "BC", "BAny", "AnyC", "CEnter",
}
history := trackTransitions(m, events)
+
// transition
m.Set(S{"C"}, nil)
+
// assert the final state
- assert.ElementsMatch(t, S{"C"}, m.ActiveStates)
+ assert.ElementsMatch(t, S{"C"}, m.activeStates)
+
+ // wait for history to drain the channel
+ time.Sleep(10 * time.Millisecond)
+
// assert the order
assert.Equal(t, events, history.Order)
// assert event counts
- for _, count := range history.Counter {
- assert.Equal(t, 1, count)
- }
+ assertEventCounts(t, history, 1)
}
func TestSingleToMultiStateTransition(t *testing.T) {
@@ -190,13 +223,15 @@ func TestSingleToMultiStateTransition(t *testing.T) {
// transition
m.Set(S{"B", "C"}, nil)
// assert the final state
- assert.ElementsMatch(t, S{"B", "C"}, m.ActiveStates)
+ assert.ElementsMatch(t, S{"B", "C"}, m.activeStates)
+
+ // wait for history to drain the channel
+ time.Sleep(10 * time.Millisecond)
+
// assert the order
assert.Equal(t, events, history.Order)
// assert event counts
- for _, count := range history.Counter {
- assert.Equal(t, 1, count)
- }
+ assertEventCounts(t, history, 1)
}
func TestMultiToMultiStateTransition(t *testing.T) {
@@ -210,13 +245,15 @@ func TestMultiToMultiStateTransition(t *testing.T) {
// transition
m.Set(S{"D", "C"}, nil)
// assert the final state
- assert.ElementsMatch(t, S{"D", "C"}, m.ActiveStates)
+ assert.ElementsMatch(t, S{"D", "C"}, m.activeStates)
+
+ // wait for history to drain the channel
+ time.Sleep(10 * time.Millisecond)
+
// assert the order
assert.Equal(t, events, history.Order)
// assert event counts
- for _, count := range history.Counter {
- assert.Equal(t, 1, count)
- }
+ assertEventCounts(t, history, 1)
}
func TestMultiToSingleStateTransition(t *testing.T) {
@@ -230,13 +267,15 @@ func TestMultiToSingleStateTransition(t *testing.T) {
// transition
m.Set(S{"C"}, nil)
// assert the final state
- assert.ElementsMatch(t, S{"C"}, m.ActiveStates)
+ assert.ElementsMatch(t, S{"C"}, m.activeStates)
+
+ // wait for history to drain the channel
+ time.Sleep(10 * time.Millisecond)
+
// assert the order
assert.Equal(t, events, history.Order)
// assert event counts
- for _, count := range history.Counter {
- assert.Equal(t, 1, count)
- }
+ assertEventCounts(t, history, 1)
}
func TestTransitionToActiveState(t *testing.T) {
@@ -248,7 +287,7 @@ func TestTransitionToActiveState(t *testing.T) {
// transition
m.Set(S{"A"}, nil)
// assert the final state
- assert.ElementsMatch(t, S{"A"}, m.ActiveStates)
+ assert.ElementsMatch(t, S{"A"}, m.activeStates)
// assert event counts (none should happen)
for _, count := range history.Counter {
assert.Equal(t, 0, count)
@@ -259,8 +298,8 @@ func TestAfterRelationWhenEntering(t *testing.T) {
m := NewNoRels(t, S{"A", "B"})
defer m.Dispose()
// relations
- m.States["C"] = am.State{After: S{"D"}}
- m.States["A"] = am.State{After: S{"B"}}
+ m.states["C"] = State{After: S{"D"}}
+ m.states["A"] = State{After: S{"B"}}
// history
events := []string{"AD", "AC", "AnyD", "DEnter", "AnyC", "CEnter"}
history := trackTransitions(m, events)
@@ -278,8 +317,8 @@ func TestAfterRelationWhenExiting(t *testing.T) {
m := NewNoRels(t, S{"A", "B"})
defer m.Dispose()
// relations
- m.States["C"] = am.State{After: S{"D"}}
- m.States["A"] = am.State{After: S{"B"}}
+ m.states["C"] = State{After: S{"D"}}
+ m.states["A"] = State{After: S{"B"}}
// history
events := []string{"BExit", "BD", "BC", "BAny", "AExit", "AD", "AC", "AAny"}
history := trackTransitions(m, events)
@@ -297,7 +336,7 @@ func TestRemoveRelation(t *testing.T) {
m := NewNoRels(t, S{"D"})
defer m.Dispose()
// relations
- m.States["C"] = am.State{Remove: S{"D"}}
+ m.states["C"] = State{Remove: S{"D"}}
// C deactivates D
m.Add(S{"C"}, nil)
assertStates(t, m, S{"C"})
@@ -311,11 +350,11 @@ func TestRemoveRelationSimultaneous(t *testing.T) {
log := ""
captureLog(t, m, &log)
// relations
- m.States["C"] = am.State{Remove: S{"D"}}
+ m.states["C"] = State{Remove: S{"D"}}
// test
r := m.Set(S{"C", "D"}, nil)
// assert
- assert.Equal(t, am.Canceled, r)
+ assert.Equal(t, Canceled, r)
assert.Contains(t, log, "[rel:remove] D by C")
assertStates(t, m, S{"D"})
}
@@ -323,11 +362,11 @@ func TestRemoveRelationSimultaneous(t *testing.T) {
func TestRemoveRelationCrossBlocking(t *testing.T) {
tests := []struct {
name string
- fn func(t *testing.T, m *am.Machine)
+ fn func(t *testing.T, m *Machine)
}{
{
"using Set should de-activate the old one",
- func(t *testing.T, m *am.Machine) {
+ func(t *testing.T, m *Machine) {
// m = (D)
m.Set(S{"C"}, nil)
assertStates(t, m, S{"C"})
@@ -335,7 +374,7 @@ func TestRemoveRelationCrossBlocking(t *testing.T) {
},
{
"using Set should work both ways",
- func(t *testing.T, m *am.Machine) {
+ func(t *testing.T, m *Machine) {
// m = (D)
m.Set(S{"C"}, nil)
assertStates(t, m, S{"C"})
@@ -345,7 +384,7 @@ func TestRemoveRelationCrossBlocking(t *testing.T) {
},
{
"using Add should de-activate the old one",
- func(t *testing.T, m *am.Machine) {
+ func(t *testing.T, m *Machine) {
// m = (D)
m.Add(S{"C"}, nil)
assertStates(t, m, S{"C"})
@@ -353,7 +392,7 @@ func TestRemoveRelationCrossBlocking(t *testing.T) {
},
{
"using Add should work both ways",
- func(t *testing.T, m *am.Machine) {
+ func(t *testing.T, m *Machine) {
// m = (D)
m.Add(S{"C"}, nil)
assertStates(t, m, S{"C"})
@@ -367,8 +406,8 @@ func TestRemoveRelationCrossBlocking(t *testing.T) {
m := NewNoRels(t, S{"D"})
defer m.Dispose()
// C and D are cross blocking each other via Remove
- m.States["C"] = am.State{Remove: S{"D"}}
- m.States["D"] = am.State{Remove: S{"C"}}
+ m.states["C"] = State{Remove: S{"D"}}
+ m.states["D"] = State{Remove: S{"C"}}
test.fn(t, m)
})
}
@@ -377,11 +416,14 @@ func TestRemoveRelationCrossBlocking(t *testing.T) {
func TestAddRelation(t *testing.T) {
m := NewNoRels(t, nil)
defer m.Dispose()
+
// relations
- m.States["A"] = am.State{Remove: S{"D"}}
- m.States["C"] = am.State{Add: S{"D"}}
+ m.states["A"] = State{Remove: S{"D"}}
+ m.states["C"] = State{Add: S{"D"}}
+
// test
m.Set(S{"C"}, nil)
+
// assert
assertStates(t, m, S{"C", "D"}, "state should be activated")
m.Set(S{"A", "C"}, nil)
@@ -394,7 +436,7 @@ func TestRequireRelation(t *testing.T) {
m := NewNoRels(t, S{"A"})
defer m.Dispose()
// relations
- m.States["C"] = am.State{Require: S{"D"}}
+ m.states["C"] = State{Require: S{"D"}}
// run the test
m.Set(S{"C", "D"}, nil)
// assert
@@ -405,7 +447,7 @@ func TestRequireRelationWhenRequiredIsntActive(t *testing.T) {
m := NewNoRels(t, S{"A"})
defer m.Dispose()
// relations
- m.States["C"] = am.State{Require: S{"D"}}
+ m.states["C"] = State{Require: S{"D"}}
// logger
log := ""
captureLog(t, m, &log)
@@ -420,7 +462,7 @@ func TestRequireRelationWhenRequiredIsntActive(t *testing.T) {
// TestQueue
type TestQueueHandlers struct{}
-func (h *TestQueueHandlers) BEnter(e *am.Event) bool {
+func (h *TestQueueHandlers) BEnter(e *Event) bool {
e.Machine.Add(S{"C"}, nil)
return true
}
@@ -445,19 +487,19 @@ func TestQueue(t *testing.T) {
// TestNegotiationCancel
type TestNegotiationCancelHandlers struct{}
-func (h *TestNegotiationCancelHandlers) DEnter(_ *am.Event) bool {
+func (h *TestNegotiationCancelHandlers) DEnter(_ *Event) bool {
return false
}
func TestNegotiationCancel(t *testing.T) {
tests := []struct {
name string
- fn func(t *testing.T, m *am.Machine) am.Result
+ fn func(t *testing.T, m *Machine) Result
log *regexp.Regexp
}{
{
"using set",
- func(t *testing.T, m *am.Machine) am.Result {
+ func(t *testing.T, m *Machine) Result {
// m = (A)
// DEnter will cancel the transition
return m.Set(S{"D"}, nil)
@@ -466,7 +508,7 @@ func TestNegotiationCancel(t *testing.T) {
},
{
"using add",
- func(t *testing.T, m *am.Machine) am.Result {
+ func(t *testing.T, m *Machine) Result {
// m = (A)
// DEnter will cancel the transition
return m.Add(S{"D"}, nil)
@@ -488,7 +530,7 @@ func TestNegotiationCancel(t *testing.T) {
// run the test
result := test.fn(t, m)
// assert
- assert.Equal(t, am.Canceled, result, "transition should be canceled")
+ assert.Equal(t, Canceled, result, "transition should be canceled")
assertStates(t, m, S{"A"}, "state shouldnt be changed")
assert.Regexp(t, test.log, log,
"log should explain the reason of cancellation")
@@ -501,7 +543,7 @@ func TestAutoStates(t *testing.T) {
m := NewNoRels(t, nil)
defer m.Dispose()
// relations
- m.States["B"] = am.State{
+ m.states["B"] = State{
Auto: true,
Require: S{"A"},
}
@@ -511,7 +553,7 @@ func TestAutoStates(t *testing.T) {
// run the test
result := m.Add(S{"A"}, nil)
// assert
- assert.Equal(t, am.Executed, result, "transition should be executed")
+ assert.Equal(t, Executed, result, "transition should be executed")
assertStates(t, m, S{"A", "B"}, "dependant auto state should be set")
assert.Contains(t, log, "[auto] B", "log should mention the auto state")
}
@@ -519,7 +561,7 @@ func TestAutoStates(t *testing.T) {
// TestNegotiationRemove
type TestNegotiationRemoveHandlers struct{}
-func (h *TestNegotiationRemoveHandlers) AExit(_ *am.Event) bool {
+func (h *TestNegotiationRemoveHandlers) AExit(_ *Event) bool {
return false
}
@@ -538,7 +580,7 @@ func TestNegotiationRemove(t *testing.T) {
// AExit will cancel the transition
result := m.Remove(S{"A"}, nil)
// assert
- assert.Equal(t, am.Canceled, result, "transition should be canceled")
+ assert.Equal(t, Canceled, result, "transition should be canceled")
assertStates(t, m, S{"A"}, "state shouldnt be changed")
assert.Regexp(t, `\[cancel] \(\) by AExit`, log,
"log should explain the reason of cancellation")
@@ -547,11 +589,11 @@ func TestNegotiationRemove(t *testing.T) {
// TestHandlerStateInfo
type TestHandlerStateInfoHandlers struct{}
-func (h *TestHandlerStateInfoHandlers) DEnter(e *am.Event) {
+func (h *TestHandlerStateInfoHandlers) DEnter(e *Event) {
t := e.Args["t"].(*testing.T)
- assert.ElementsMatch(t, S{"A"}, e.Machine.ActiveStates,
+ assert.ElementsMatch(t, S{"A"}, e.Machine.ActiveStates(),
"provide the previous states of the transition")
- assert.ElementsMatch(t, S{"D"}, e.Machine.To(),
+ assert.ElementsMatch(t, S{"D"}, e.Transition().TargetStates,
"provide the target states of the transition")
}
@@ -579,19 +621,19 @@ func TestHandlerStateInfo(t *testing.T) {
// TestHandlerArgs
type TestHandlerArgsHandlers struct{}
-func (h *TestHandlerArgsHandlers) BEnter(e *am.Event) {
+func (h *TestHandlerArgsHandlers) BEnter(e *Event) {
t := e.Args["t"].(*testing.T)
foo := e.Args["foo"].(string)
assert.Equal(t, "bar", foo)
}
-func (h *TestHandlerArgsHandlers) AExit(e *am.Event) {
+func (h *TestHandlerArgsHandlers) AExit(e *Event) {
t := e.Args["t"].(*testing.T)
foo := e.Args["foo"].(string)
assert.Equal(t, "bar", foo)
}
-func (h *TestHandlerArgsHandlers) CState(e *am.Event) {
+func (h *TestHandlerArgsHandlers) CState(e *Event) {
t := e.Args["t"].(*testing.T)
foo := e.Args["foo"].(string)
assert.Equal(t, "bar", foo)
@@ -620,7 +662,7 @@ func TestHandlerArgs(t *testing.T) {
// TestSelfHandlersCancellable
type TestSelfHandlersCancellableHandlers struct{}
-func (h *TestSelfHandlersCancellableHandlers) AA(e *am.Event) bool {
+func (h *TestSelfHandlersCancellableHandlers) AA(e *Event) bool {
AACounter := e.Args["AACounter"].(*int)
*AACounter++
return false
@@ -650,11 +692,18 @@ func TestSelfHandlersOrder(t *testing.T) {
// init
m := NewNoRels(t, S{"A"})
defer m.Dispose()
+ m.SetLogLevel(LogEverything)
+
// bind history
events := []string{"AA", "AnyB", "BEnter"}
history := trackTransitions(m, events)
+
// run the test
m.Set(S{"A", "B"}, nil)
+
+ // wait for history to drain the channel
+ time.Sleep(10 * time.Millisecond)
+
// assert
assert.Equal(t, events, history.Order)
}
@@ -669,6 +718,10 @@ func TestSelfHandlersForCalledOnly(t *testing.T) {
// run the test
m.Add(S{"B"}, nil)
m.Add(S{"A"}, nil)
+
+ // wait for history to drain the channel
+ time.Sleep(10 * time.Millisecond)
+
// assert
assert.Equal(t, 1, history.Counter["AA"], "AA call count")
assert.Equal(t, 0, history.Counter["BB"], "BB call count")
@@ -676,7 +729,7 @@ func TestSelfHandlersForCalledOnly(t *testing.T) {
func TestRegressionRemoveCrossBlockedByImplied(t *testing.T) {
// init
- m := NewCustomStates(t, am.States{
+ m := NewCustomStates(t, Struct{
"A": {Remove: S{"B"}},
"B": {Remove: S{"A"}},
"Z": {Add: S{"B"}},
@@ -690,7 +743,7 @@ func TestRegressionRemoveCrossBlockedByImplied(t *testing.T) {
func TestRegressionImpliedBlockByBeingRemoved(t *testing.T) {
// init
- m := NewCustomStates(t, am.States{
+ m := NewCustomStates(t, Struct{
"Wet": {Require: S{"Water"}},
"Dry": {Remove: S{"Wet"}},
"Water": {Add: S{"Wet"}, Remove: S{"Dry"}},
@@ -706,7 +759,7 @@ func TestRegressionImpliedBlockByBeingRemoved(t *testing.T) {
// TestWhen
type TestWhenHandlers struct{}
-func (h *TestWhenHandlers) AState(e *am.Event) {
+func (h *TestWhenHandlers) AState(e *Event) {
go func() {
time.Sleep(10 * time.Millisecond)
e.Machine.Add(S{"B"}, nil)
@@ -715,10 +768,12 @@ func (h *TestWhenHandlers) AState(e *am.Event) {
}()
}
+// TODO
func TestWhen(t *testing.T) {
// init
m := NewNoRels(t, nil)
defer m.Dispose()
+
// bind handlers
err := m.BindHandlers(&TestWhenHandlers{})
assert.NoError(t, err)
@@ -726,6 +781,7 @@ func TestWhen(t *testing.T) {
// run the test
m.Set(S{"A"}, nil)
<-m.When(S{"B", "C"}, nil)
+
// assert
assertStates(t, m, S{"A", "B", "C"})
}
@@ -734,8 +790,10 @@ func TestWhenActive(t *testing.T) {
// init
m := NewNoRels(t, S{"A"})
defer m.Dispose()
+
// run the test
<-m.When(S{"A"}, nil)
+
// assert
assertStates(t, m, S{"A"})
}
@@ -743,7 +801,7 @@ func TestWhenActive(t *testing.T) {
// TestWhenNot
type TestWhenNotHandlers struct{}
-func (h *TestWhenNotHandlers) AState(e *am.Event) {
+func (h *TestWhenNotHandlers) AState(e *Event) {
go func() {
time.Sleep(10 * time.Millisecond)
e.Machine.Remove(S{"B"}, nil)
@@ -780,10 +838,10 @@ func TestWhenNotActive(t *testing.T) {
// TestPartialNegotiationPanic
type TestPartialNegotiationPanicHandlers struct {
- am.ExceptionHandler
+ ExceptionHandler
}
-func (h *TestPartialNegotiationPanicHandlers) BEnter(_ *am.Event) {
+func (h *TestPartialNegotiationPanicHandlers) BEnter(_ *Event) {
panic("BEnter panic")
}
@@ -799,7 +857,7 @@ func TestPartialNegotiationPanic(t *testing.T) {
assert.NoError(t, err)
// run the test
- assert.Equal(t, am.Canceled, m.Add(S{"B"}, nil))
+ assert.Equal(t, Canceled, m.Add(S{"B"}, nil))
// assert
assertStates(t, m, S{"A", "Exception"})
assert.Regexp(t, `\[cancel] \(B A\) by recover`, log,
@@ -808,10 +866,10 @@ func TestPartialNegotiationPanic(t *testing.T) {
// TestPartialFinalPanic
type TestPartialFinalPanicHandlers struct {
- am.ExceptionHandler
+ ExceptionHandler
}
-func (h *TestPartialFinalPanicHandlers) BState(_ *am.Event) {
+func (h *TestPartialFinalPanicHandlers) BState(_ *Event) {
panic("BState panic")
}
@@ -840,14 +898,14 @@ func TestPartialFinalPanic(t *testing.T) {
// TestStateCtx
type TestStateCtxHandlers struct {
- am.ExceptionHandler
+ ExceptionHandler
callbackCh chan bool
}
-func (h *TestStateCtxHandlers) AState(e *am.Event) {
+func (h *TestStateCtxHandlers) AState(e *Event) {
t := e.Args["t"].(*testing.T)
stepCh := e.Args["stepCh"].(chan bool)
- stateCtx := e.Machine.GetStateCtx("A")
+ stateCtx := e.Machine.NewStateCtx("A")
h.callbackCh = make(chan bool)
go func() {
<-stepCh
@@ -881,23 +939,23 @@ func TestStateCtx(t *testing.T) {
// TestQueueCheckable
type TestQueueCheckableHandlers struct {
- am.ExceptionHandler
+ ExceptionHandler
assertsCount int
}
-func (h *TestQueueCheckableHandlers) AState(e *am.Event) {
+func (h *TestQueueCheckableHandlers) AState(e *Event) {
t := e.Args["t"].(*testing.T)
e.Machine.Add(S{"B"}, nil)
e.Machine.Add(S{"C"}, nil)
e.Machine.Add(S{"D"}, nil)
- assert.Len(t, e.Machine.Queue, 3, "queue should have 3 mutations scheduled")
+ assert.Len(t, e.Machine.queue, 3, "queue should have 3 mutations scheduled")
h.assertsCount++
assert.Equal(t, 1,
- e.Machine.IsQueued(am.MutationTypeAdd, S{"C"}, false, false, 0),
+ e.Machine.IsQueued(MutationAdd, S{"C"}, false, false, 0),
"C should be queued")
h.assertsCount++
assert.Equal(t, -1,
- e.Machine.IsQueued(am.MutationTypeAdd, S{"A"}, false, false, 0),
+ e.Machine.IsQueued(MutationAdd, S{"A"}, false, false, 0),
"A should NOT be queued")
h.assertsCount++
}
@@ -923,11 +981,11 @@ func TestPartialAuto(t *testing.T) {
m := NewNoRels(t, nil)
defer m.Dispose()
// relations
- m.States["C"] = am.State{
+ m.states["C"] = State{
Auto: true,
Require: S{"B"},
}
- m.States["D"] = am.State{
+ m.states["D"] = State{
Auto: true,
Require: S{"B"},
}
@@ -941,44 +999,41 @@ func TestPartialAuto(t *testing.T) {
assert.Regexp(t, `\[cancel:reject\] [C D]{3}`, log)
}
-func TestClock(t *testing.T) {
+func TestTime(t *testing.T) {
// init
m := NewNoRels(t, nil)
// relations
- m.States["B"] = am.State{Multi: true}
+ m.states["B"] = State{Multi: true}
// test 1
// ()[]
m.Add(S{"A", "B"}, nil)
- // (A B)[A1 B1]
assertStates(t, m, S{"A", "B"})
+ assertTimes(t, m, S{"A", "B", "C", "D"}, T{1, 1, 0, 0})
m.Add(S{"A", "B"}, nil)
- // (A B)[A1 B2]
assertStates(t, m, S{"A", "B"})
+ assertTimes(t, m, S{"A", "B", "C", "D"}, T{1, 3, 0, 0})
m.Add(S{"A", "B", "C"}, nil)
- // (A B C)[A1 B3 C1]
assertStates(t, m, S{"A", "B", "C"})
+ assertTimes(t, m, S{"A", "B", "C", "D"}, T{1, 5, 1, 0})
m.Set(S{"D"}, nil)
- // (D)[A1 B3 C1 D1]
assertStates(t, m, S{"D"})
+ assertTimes(t, m, S{"A", "B", "C", "D"}, T{2, 6, 2, 1})
m.Add(S{"D", "C"}, nil)
- // (D C)[A1 B3 C2 D1]
assertStates(t, m, S{"D", "C"})
+ assertTimes(t, m, S{"A", "B", "C", "D"}, T{2, 6, 3, 1})
m.Remove(S{"B", "C"}, nil)
- // (D)[A1 B3 C2 D1]
assertStates(t, m, S{"D"})
+ assertTimes(t, m, S{"A", "B", "C", "D"}, T{2, 6, 4, 1})
m.Add(S{"A", "B"}, nil)
- // (D A B)[A2 B4 C2 D1]
assertStates(t, m, S{"D", "A", "B"})
-
- // assert
- assert.Equal(t, m.Time(S{"A", "B", "C", "D"}), T{2, 4, 2, 1})
+ assertTimes(t, m, S{"A", "B", "C", "D"}, T{3, 7, 4, 1})
// test 2
order := S{"A", "B", "C", "D"}
@@ -988,55 +1043,228 @@ func TestClock(t *testing.T) {
// assert
assertStates(t, m, S{"A", "B", "D", "C"})
- assert.Equal(t, m.Time(order), T{2, 4, 3, 1})
- assert.True(t, am.IsTimeAfter(now, before))
- assert.False(t, am.IsTimeAfter(before, now))
+ assertTimes(t, m, S{"A", "B", "C", "D"}, T{3, 7, 5, 1})
+ assert.True(t, IsTimeAfter(now, before))
+ assert.False(t, IsTimeAfter(before, now))
}
func TestWhenCtx(t *testing.T) {
- // TODO TestWhenCtx
+ // init
+ m := NewNoRels(t, S{"A", "B"})
+ defer m.Dispose()
+
// wait on 2 Whens with a step context
- // make sure everything gets disposed
+ ctx, cancel := context.WithCancel(context.Background())
+ whenTimeCh := m.WhenTime(S{"A", "B"}, T{3, 3}, ctx)
+ whenArgsCh := m.WhenArgs("B", A{"foo": "bar"}, ctx)
+ whenCh := m.When1("C", ctx)
+
+ // assert
+ assert.Greater(t, len(m.indexWhenTime), 0)
+ assert.Greater(t, len(m.indexWhen), 0)
+ assert.Greater(t, len(m.indexWhenArgs), 0)
+
+ go func() {
+ time.Sleep(10 * time.Millisecond)
+ cancel()
+ }()
+
+ select {
+ case <-whenCh:
+ case <-whenArgsCh:
+ case <-whenTimeCh:
+ case <-ctx.Done():
+ }
+
+ // wait for the context to be canceled and cleanups happen
+ time.Sleep(time.Millisecond)
+
+ // assert
+ assert.Equal(t, len(m.indexWhenTime), 0)
+ assert.Equal(t, len(m.indexWhen), 0)
+ assert.Equal(t, len(m.indexWhenArgs), 0)
+}
+
+func TestWhenArgs(t *testing.T) {
+ // init
+ m := NewRels(t, nil)
+ defer m.Dispose()
+
+ // bind
+ whenCh := m.WhenArgs("B", A{"foo": "bar"}, nil)
+
+ // incorrect args
+ m.Add1("B", A{"foo": "foo"})
+ select {
+ case <-whenCh:
+ t.Fatal("when shouldnt be resolved")
+ default:
+ // pass
+ }
+
+ // correct args
+ m.Add1("B", A{"foo": "bar"})
+ select {
+ default:
+ t.Fatal("when should be resolved")
+ case <-whenCh:
+ // pass
+ }
+}
+
+func TestWhenTime(t *testing.T) {
+ // init
+ m := NewNoRels(t, S{"A", "B"})
+ defer m.Dispose()
+
+ // bind
+ // (A:1 B:1)
+ whenCh := m.WhenTime(S{"A", "B"}, T{5, 2}, nil)
+
+ // tick some, but not enough
+ m.Remove(S{"A", "B"}, nil)
+ m.Add(S{"A", "B"}, nil)
+ // (A:3 B:3) not yet
+ select {
+ case <-whenCh:
+ t.Fatal("when shouldnt be resolved")
+ default:
+ // pass
+ }
+
+ m.Remove1("A", nil)
+ m.Add1("A", nil)
+ // (A:5 B:3) OK
+
+ select {
+ default:
+ t.Fatal("when should be resolved")
+ case <-whenCh:
+ // pass
+ }
+}
+
+func TestNewCommon(t *testing.T) {
+ // TODO TestConfig
t.Skip()
}
-func TestConfig(t *testing.T) {
+func TestTracers(t *testing.T) {
// TODO TestConfig
t.Skip()
}
-func TestPanic(t *testing.T) {
- // TODO TestPanic
+func TestQueueLimit(t *testing.T) {
+ // TODO TestConfig
t.Skip()
}
-func TestIs(t *testing.T) {
- // TODO TestIs
+func TestSubmachines(t *testing.T) {
+ // TODO TestSubmachines
t.Skip()
}
-func TestNot(t *testing.T) {
- // TODO TestNot
+func TestEval(t *testing.T) {
+ // TODO TestSubmachines
t.Skip()
}
+func TestSetStates(t *testing.T) {
+ // init
+ m := NewNoRels(t, S{"A", "C"})
+ defer m.Dispose()
+
+ // add relations and states
+ s := m.GetStruct()
+ s["A"] = State{Multi: true}
+ s["B"] = State{Remove: S{"C"}}
+ s["D"] = State{Add: S{"E"}}
+ s["E"] = State{}
+
+ // update states
+ err := m.SetStruct(s, S{"A", "B", "C", "D", "E"})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // test
+ m.Set(S{"A", "B", "D"}, nil)
+
+ // assert
+ assert.ElementsMatch(t, S{"A", "B", "D", "E"}, m.activeStates)
+}
+
+func TestIs(t *testing.T) {
+ // init
+ m := NewNoRels(t, S{"A", "B"})
+ defer m.Dispose()
+
+ // test
+ assert.True(t, m.Is(S{"A", "B"}), "A B should be active")
+ assert.False(t, m.Is(S{"A", "B", "C"}), "A B C shouldnt be active")
+}
+
+func TestNot(t *testing.T) {
+ // init
+ m := NewNoRels(t, S{"A", "B"})
+ defer m.Dispose()
+
+ // test
+ assert.False(t, m.Not(S{"A", "B"}), "A B should be active")
+ assert.False(t, m.Not(S{"A", "B", "C"}), "A B C is partially active")
+ assert.True(t, m.Not1("D"), "D is inactive")
+}
+
func TestAny(t *testing.T) {
- // TODO TestAny
- t.Skip()
+ // init
+ m := NewNoRels(t, S{"A", "B"})
+ defer m.Dispose()
+
+ // test
+ assert.True(t, m.Any(S{"A", "B"}, S{"C"}), "A B should be active")
+ assert.True(t, m.Any(S{"A", "B", "C"}, S{"A"}), "A B C is partially active")
}
func TestIsClock(t *testing.T) {
- // TODO TestIsClock
- t.Skip()
-}
+ // init
+ m := NewNoRels(t, nil)
+ // relations
+ m.states["B"] = State{Multi: true}
-func TestDynamicHandlers(t *testing.T) {
- // TODO TestDynamicHandlers On(), On(..., ctx)
- t.Skip()
+ // test 1
+ // ()[]
+ m.Add(S{"A", "B"}, nil)
+ assertStates(t, m, S{"A", "B"})
+ assertClocks(t, m, S{"A", "B", "C", "D"}, T{1, 1, 0, 0})
+
+ m.Add(S{"A", "B"}, nil)
+ assertStates(t, m, S{"A", "B"})
+ assertClocks(t, m, S{"A", "B", "C", "D"}, T{1, 3, 0, 0})
+
+ m.Add(S{"A", "B", "C"}, nil)
+ assertStates(t, m, S{"A", "B", "C"})
+ assertClocks(t, m, S{"A", "B", "C", "D"}, T{1, 5, 1, 0})
+
+ m.Set(S{"D"}, nil)
+ assertStates(t, m, S{"D"})
+ assertClocks(t, m, S{"A", "B", "C", "D"}, T{2, 6, 2, 1})
+
+ m.Add(S{"D", "C"}, nil)
+ assertStates(t, m, S{"D", "C"})
+ assertClocks(t, m, S{"A", "B", "C", "D"}, T{2, 6, 3, 1})
+
+ m.Remove(S{"B", "C"}, nil)
+ assertStates(t, m, S{"D"})
+ assertClocks(t, m, S{"A", "B", "C", "D"}, T{2, 6, 4, 1})
+
+ m.Add(S{"A", "B"}, nil)
+ assertStates(t, m, S{"D", "A", "B"})
+ assertClocks(t, m, S{"A", "B", "C", "D"}, T{3, 7, 4, 1})
}
func TestInspect(t *testing.T) {
m := NewRels(t, S{"A", "C"})
+ // (A:1 C:1)[B:0 D:0 Exception:0]
names := S{"A", "B", "C", "D", "Exception"}
expected := `
A:
@@ -1062,12 +1290,16 @@ func TestInspect(t *testing.T) {
Multi: true
`
assertString(t, m, expected, names)
- m.Add(S{"B"}, nil)
+ // (A:1 C:1)[B:0 D:0 Exception:0]
m.Remove(S{"C"}, nil)
+ // ()[A:2 B:0 C:2 D:0 Exception:0]
+ m.Add(S{"B"}, nil)
+ // (A:3 B:1 C:3)[D:0 Exception:0]
m.Add(S{"D"}, nil)
+ // (A:3 B:1 C:3 D:1)[Exception:0]
expected = `
A:
- State: true 2
+ State: true 3
Auto: true
Require: C
@@ -1077,7 +1309,7 @@ func TestInspect(t *testing.T) {
Add: C
C:
- State: true 2
+ State: true 3
After: D
D:
diff --git a/pkg/machine/misc.go b/pkg/machine/misc.go
index 61857bd..4e3ef24 100644
--- a/pkg/machine/misc.go
+++ b/pkg/machine/misc.go
@@ -2,20 +2,25 @@ package machine
import (
"context"
+ "crypto/rand"
+ "encoding/hex"
+ "errors"
+ "fmt"
"reflect"
+ "regexp"
+ "slices"
"strings"
"time"
-
- "github.com/samber/lo"
)
// S (state names) is a string list of state names.
type S []string
-// A (arguments) is a map of named method arguments.
+// A (arguments) is a map of named arguments for a Mutation.
type A map[string]any
// T is an ordered list of state clocks.
+// TODO use math/big?
type T []uint64
// Clocks is a map of state names to their clocks.
@@ -31,19 +36,16 @@ type State struct {
After S
}
-// States is a map of state names to state definitions.
-type States = map[string]State
+// Struct is a map of state names to state definitions.
+type Struct = map[string]State
-type Event struct {
- Name string
- Machine *Machine
- Args A
- // internal events lack a step
- step *TransitionStep
-}
+///////////////
+///// options
+///////////////
+// Opts struct is used to configure a new Machine.
type Opts struct {
- // Unique ID of this machine. Default: random UUID.
+ // Unique ID of this machine. Default: random ID.
ID string
// Time for a handler to execute. Default: time.Second
HandlerTimeout time.Duration
@@ -57,17 +59,90 @@ type Opts struct {
Resolver RelationsResolver
// Log level of the machine. Default: LogNothing.
LogLevel LogLevel
+ // Tracer for the machine. Default: nil.
+ Tracers []Tracer
+ // LogArgs matching function for the machine. Default: nil.
+ LogArgs func(args A) map[string]string
+ // Parent machine, used to inherit certain properties, e.g. tracers.
+ // Overrides ParentID. Default: nil.
+ Parent *Machine
+ // ParentID is the ID of the parent machine. Default: "".
+ ParentID string
+ // Tags is a list of tags for the machine. Default: nil.
+ Tags []string
+ // QueueLimit is the maximum number of mutations that can be queued.
+ // Default: 1000.
+ // TODO per-state QueueLimit
+ QueueLimit int
+}
+
+// OptsWithDebug returns Opts with debug settings (DontPanicToException,
+// long HandlerTimeout).
+func OptsWithDebug(opts *Opts) *Opts {
+ opts.DontPanicToException = true
+ opts.HandlerTimeout = 10 * time.Minute
+
+ return opts
+}
+
+// OptsWithTracers returns Opts with the given tracers. Tracers are inherited
+// by submachines (via Opts.Parent) when env.AM_DEBUG is set.
+func OptsWithTracers(opts *Opts, tracers ...Tracer) *Opts {
+ if tracers != nil {
+ opts.Tracers = tracers
+ }
+
+ return opts
+}
+
+// OptsWithParentTracers returns Opts with the parent's Opts.Tracers and
+// Opts.LogArgs.
+func OptsWithParentTracers(opts *Opts, parent *Machine) *Opts {
+ var tracers []Tracer
+
+ tracers = append(tracers, parent.Tracers...)
+ opts.Tracers = tracers
+ opts.LogArgs = parent.GetLogArgs()
+
+ return opts
}
+///////////////
+///// enums
+///////////////
+
// Result enum is the result of a state Transition
type Result int
const (
+ // Executed means that the transition was executed immediately and not
+ // canceled.
Executed Result = 1 << iota
+ // Canceled means that the transition was canceled, by either relations or a
+ // handler.
Canceled
+ // Queued means that the transition was queued for later execution. The
+ // following methods can be used to wait for the results:
+ // - Machine.When
+ // - Machine.WhenNot
+ // - Machine.WhenArgs
+ // - Machine.WhenTime
Queued
)
+var (
+ // ErrStateUnknown indicates that the state is unknown.
+ ErrStateUnknown = errors.New("state unknown")
+ // ErrCanceled can be used to indicate a canceled Transition. Not used ATM.
+ ErrCanceled = errors.New("transition canceled")
+ // ErrQueued can be used to indicate a queued Transition. Not used ATM.
+ ErrQueued = errors.New("transition queued")
+ // ErrInvalidArgs can be used to indicate invalid arguments. Not used ATM.
+ ErrInvalidArgs = errors.New("invalid arguments")
+ // ErrTimeout can be used to indicate timed out mutation. Not used ATM.
+ ErrTimeout = errors.New("timeout")
+)
+
func (r Result) String() string {
switch r {
case Executed:
@@ -81,84 +156,116 @@ func (r Result) String() string {
}
const (
- EventTransitionEnd string = "transition-end"
- EventTransitionStart string = "transition-start"
- EventTransitionCancel string = "transition-cancel"
- EventQueueEnd string = "queue-end"
- EventTick string = "tick"
+ // EventTransitionEnd transition ended
+ EventTransitionEnd = "transition-end"
+ // EventTransitionInit transition being initialized
+ EventTransitionInit = "transition-init"
+ // EventTransitionStart transition started
+ EventTransitionStart = "transition-start"
+ // EventTransitionCancel transition canceled
+ EventTransitionCancel = "transition-cancel"
+ // EventQueueEnd queue fully drained
+ EventQueueEnd = "queue-end"
+ // EventQueueAdd new mutation queued
+ EventQueueAdd = "queue-add"
+ // EventStructChange machine's states structure have changed
+ EventStructChange = "struct-change"
+ // EventTick active states changed, at least one state clock increased
+ EventTick = "tick"
+ // EventException Exception state becomes active
+ EventException = "exception"
)
// MutationType enum
type MutationType int
const (
- MutationTypeAdd MutationType = iota
- MutationTypeRemove
- MutationTypeSet
+ MutationAdd MutationType = iota
+ MutationRemove
+ MutationSet
+ MutationEval
)
func (m MutationType) String() string {
switch m {
- case MutationTypeAdd:
+ case MutationAdd:
return "add"
- case MutationTypeRemove:
+ case MutationRemove:
return "remove"
- case MutationTypeSet:
+ case MutationSet:
return "set"
+ case MutationEval:
+ return "eval"
}
return ""
}
+// Mutation represents an atomic change (or an attempt) of machine's active
+// states. Mutation causes a Transition.
type Mutation struct {
- Type MutationType
+ // add, set, remove
+ Type MutationType
+ // states explicitly passed to the mutation method
CalledStates S
- Args A
+ // argument map passed to the mutation method (if any).
+ Args A
// this mutation has been triggered by an auto state
Auto bool
+ // specific context for this mutation (optional)
+ Ctx context.Context
+ // optional eval func, only for MutationEval
+ Eval func()
}
-// TransitionStepType enum
-// TODO rename to StepType
-type TransitionStepType int
+// StateWasCalled returns true if the Mutation was called with the passed
+// state (or does it come from relations).
+func (m Mutation) StateWasCalled(state string) bool {
+ return slices.Contains(m.CalledStates, state)
+}
+
+// StepType enum
+type StepType int
const (
- TransitionStepTypeRelation TransitionStepType = 1 << iota
- TransitionStepTypeTransition
- TransitionStepTypeSet
- TransitionStepTypeRemove
- // TODO rename to StepTypeRemoveNotSet
- TransitionStepTypeNoSet
- TransitionStepTypeRequested
- TransitionStepTypeCancel
+ StepRelation StepType = 1 << iota
+ StepHandler
+ StepSet
+ // StepRemove indicates a step where a state goes active->inactive
+ StepRemove
+ // StepRemoveNotActive indicates a step where a state goes inactive->inactive
+ StepRemoveNotActive
+ StepRequested
+ StepCancel
)
-func (tt TransitionStepType) String() string {
+func (tt StepType) String() string {
switch tt {
- case TransitionStepTypeRelation:
+ case StepRelation:
return "rel"
- case TransitionStepTypeTransition:
- // TODO rename to TransitionStepTypeHandler ?
- return "transition"
- case TransitionStepTypeSet:
+ case StepHandler:
+ return "handler"
+ case StepSet:
return "set"
- case TransitionStepTypeRemove:
+ case StepRemove:
return "remove"
- case TransitionStepTypeNoSet:
- // TODO rename to TransitionStepTypeRemoveTry ?
- return "no-set"
- case TransitionStepTypeRequested:
+ case StepRemoveNotActive:
+ return "removenotactive"
+ case StepRequested:
return "requested"
- case TransitionStepTypeCancel:
+ case StepCancel:
return "cancel"
}
return ""
}
-type TransitionStep struct {
- ID string
+// Step struct represents a single step within a Transition, either a relation
+// resolving step or a handler call.
+type Step struct {
+ Type StepType
+ // optional, unless no ToState
FromState string
- ToState string
- Type TransitionStepType
+ // optional, unless no FromState
+ ToState string
// eg a transition method name, relation type
Data any
// marks a final handler (FooState, FooEnd)
@@ -193,20 +300,26 @@ func (r Relation) String() string {
return ""
}
-type HandlerBinding struct {
- Ready chan struct{}
-}
+///////////////
+///// logging
+///////////////
+// Logger is a logging function for the machine.
type Logger func(level LogLevel, msg string, args ...any)
// LogLevel enum
type LogLevel int
const (
+ // LogNothing means no logging, including external msgs.
LogNothing LogLevel = iota
+ // LogChanges means logging state changes and external msgs.
LogChanges
+ // LogOps means LogChanges + logging all the operations.
LogOps
+ // LogDecisions means LogOps + logging all the decisions behind them.
LogDecisions
+ // LogEverything means LogDecisions + all event and emitter names, and more.
LogEverything
)
@@ -226,22 +339,120 @@ func (l LogLevel) String() string {
return "nothing"
}
+// NewArgsMapper returns a matcher function for LogArgs. Useful for debugging
+// untyped argument maps.
+//
+// maxlen: maximum length of the string representation of the argument
+// (default=20).
+func NewArgsMapper(names []string, maxlen int) func(args A) map[string]string {
+ if maxlen == 0 {
+ maxlen = 20
+ }
+ return func(args A) map[string]string {
+ oks := make([]bool, len(names))
+ found := 0
+ for i, name := range names {
+ _, ok := args[name]
+ oks[i] = ok
+ found++
+ }
+ if found == 0 {
+ return nil
+ }
+ ret := make(map[string]string)
+ for i, name := range names {
+ if !oks[i] {
+ continue
+ }
+ ret[name] = truncateStr(fmt.Sprintf("%v", args[name]), maxlen)
+ }
+ return ret
+ }
+}
+
+// Tracer is an interface for logging machine transitions and events, used by
+// Opts.Tracers.
+type Tracer interface {
+ TransitionInit(transition *Transition)
+ TransitionEnd(transition *Transition)
+ HandlerStart(transition *Transition, emitter string, handler string)
+ HandlerEnd(transition *Transition, emitter string, handler string)
+ End()
+ MachineInit(mach *Machine)
+ MachineDispose(machID string)
+ NewSubmachine(parent, mach *Machine)
+ Inheritable() bool
+}
+
+///////////////
+///// events, when, emitters
+///////////////
+
+// Event struct represents a single event of a Mutation withing a Transition.
+// One event can have 0-n handlers.
+type Event struct {
+ // Name of the event / handler
+ Name string
+ // Machine is the machine that the event belongs to, it can be used to access
+ // the current Transition and Mutation.
+ Machine *Machine
+ // Args is a map of named arguments for a Mutation.
+ Args A
+ // internal events lack a step
+ step *Step
+}
+
+// Mutation returns the Mutation of an Event.
+func (e *Event) Mutation() *Mutation {
+ return e.Machine.Transition.Mutation
+}
+
+// Transition returns the Transition of an Event.
+func (e *Event) Transition() *Transition {
+ return e.Machine.Transition
+}
+
type (
- // map of (single) state names to a list of bindings
+ // map of (single) state names to a list of activation / de-activation
+ // bindings
indexWhen map[string][]*whenBinding
- // map of (single) state names to a list of bindings
+ // map of (single) state names to a list of time bindings
+ indexWhenTime map[string][]*whenTimeBinding
+ // map of (single) state names to a list of args value bindings
+ indexWhenArgs map[string][]*whenArgsBinding
+ // map of (single) state names to a context cancel function
indexStateCtx map[string][]context.CancelFunc
- indexEventCh map[string][]chan *Event
+ // map of (single) state names to an event channel
+ indexEventCh map[string][]chan *Event
)
type whenBinding struct {
- ch chan struct{}
+ ch chan struct{}
+ // means states are required to NOT be active
negation bool
states stateIsActive
matched int
total int
}
+type whenTimeBinding struct {
+ ch chan struct{}
+ // map of completed to their index positions
+ index map[string]int
+ // number of matches so far
+ matched int
+ // number of total matches needed
+ total int
+ // optional times to match for completed from index
+ times T
+ completed stateIsActive
+}
+
+type whenArgsBinding struct {
+ ch chan struct{}
+ args A
+}
+
type stateIsActive map[string]bool
// emitter represents a single event consumer, synchronized by channels.
@@ -251,59 +462,32 @@ type emitter struct {
methods *reflect.Value
}
-// newEmitter creates a new emitter for Machine.
-// Each emitter should be consumed by one receiver only to guarantee the
-// delivery of all events.
-func (m *Machine) newEmitter(name string, methods *reflect.Value) *emitter {
- e := &emitter{
- id: name,
- methods: methods,
- }
- // TODO emitter mutex
- m.emitters = append(m.emitters, e)
- return e
-}
-
func (e *emitter) dispose() {
e.disposed = true
e.methods = nil
}
-// DiffStates returns the states that are in states1 but not in states2.
-func DiffStates(states1 S, states2 S) S {
- return lo.Filter(states1, func(name string, i int) bool {
- return !lo.Contains(states2, name)
- })
-}
-
-// IsTimeAfter checks if time1 is after time2. Requires ordered results from
-// Machine.Time() (with specified states).
-func IsTimeAfter(time1, time2 T) bool {
- after := false
- for i, t1 := range time1 {
- if t1 < time2[i] {
- return false
- }
- if t1 > time2[i] {
- after = true
- }
- }
- return after
-}
+///////////////
+///// exception support
+///////////////
-// ExceptionHandler provide basic Exception state support.
-type ExceptionHandler struct{}
+// Exception is the Exception state name
+const Exception = "Exception"
-// ExceptionArgsPanic is an optional argument for the Exception state which
-// describes a panic within a transition handler.
+// ExceptionArgsPanic is an optional argument ["panic"] for the Exception state
+// which describes a panic within a Transition handler.
type ExceptionArgsPanic struct {
CalledStates S
StatesBefore S
Transition *Transition
- LastStep *TransitionStep
+ LastStep *Step
StackTrace []byte
}
+// ExceptionHandler provide a basic Exception state support, as should be
+// embedded into handler structs in most of the cases.
+type ExceptionHandler struct{}
+
// ExceptionState is a final entry handler for the Exception state.
// Args:
// - err error: The error that caused the Exception state.
@@ -311,9 +495,10 @@ type ExceptionArgsPanic struct {
func (eh *ExceptionHandler) ExceptionState(e *Event) {
if e.Machine.PrintExceptions {
err := e.Args["err"].(error)
- _, detailsOK := e.Args["panic"]
- if !detailsOK {
- e.Machine.log(LogChanges, "[error:%s] %s (%s)", err)
+ _, ok := e.Args["panic"]
+ if !ok {
+ // TODO more mutation info
+ e.Machine.log(LogChanges, "[error] %s", err)
return
}
details := e.Args["panic"].(*ExceptionArgsPanic)
@@ -321,19 +506,107 @@ func (eh *ExceptionHandler) ExceptionState(e *Event) {
e.Machine.log(LogChanges, "[error:%s] %s (%s)", mutType,
j(details.CalledStates), err)
if details.StackTrace != nil {
- e.Machine.log(LogEverything, "[error:trace] %s", details.StackTrace)
+ e.Machine.log(LogChanges, "[error:trace] %s", details.StackTrace)
+ }
+ }
+}
+
+///////////////
+///// pub utils
+///////////////
+
+// DiffStates returns the states that are in states1 but not in states2.
+func DiffStates(states1 S, states2 S) S {
+ return slicesFilter(states1, func(name string, i int) bool {
+ return !slices.Contains(states2, name)
+ })
+}
+
+// IsTimeAfter checks if time1 is after time2. Requires a deterministic states
+// order, e.g. by using Machine.VerifyStates.
+func IsTimeAfter(time1, time2 T) bool {
+ after := false
+ for i, t1 := range time1 {
+ if t1 < time2[i] {
+ return false
+ }
+ if t1 > time2[i] {
+ after = true
}
}
+ return after
}
-// utils
+// CloneStates deep clones the states struct and returns a copy.
+func CloneStates(states Struct) Struct {
+ ret := make(Struct)
-// j joins state names
+ for name, state := range states {
+
+ stateCopy := State{
+ Auto: state.Auto,
+ Multi: state.Multi,
+ }
+
+ // TODO move to Resolver
+
+ if state.Require != nil {
+ stateCopy.Require = slices.Clone(state.Require)
+ }
+ if state.Add != nil {
+ stateCopy.Add = slices.Clone(state.Add)
+ }
+ if state.Remove != nil {
+ stateCopy.Remove = slices.Clone(state.Remove)
+ }
+ if state.After != nil {
+ stateCopy.After = slices.Clone(state.After)
+ }
+
+ ret[name] = stateCopy
+ }
+
+ return ret
+}
+
+// IsActiveTick returns true if the tick represents an active state
+// (odd number).
+func IsActiveTick(tick uint64) bool {
+ return tick%2 == 1
+}
+
+// everything else than a-z and _
+var invalidName = regexp.MustCompile("[^a-z_0-9]+")
+
+func NormalizeID(id string) string {
+ return invalidName.ReplaceAllString(id, "_")
+}
+
+// SMerge merges multiple state lists into one, removing duplicates.
+// Especially useful for merging state group with single states.
+func SMerge(states ...S) S {
+ if len(states) == 0 {
+ return S{}
+ }
+
+ s := slices.Clone(states[0])
+ for i := 1; i < len(states); i++ {
+ s = append(s, states[i]...)
+ }
+
+ return slicesUniq(s)
+}
+
+///////////////
+///// utils
+///////////////
+
+// j joins state names into a single string
func j(states []string) string {
return strings.Join(states, " ")
}
-// jw joins state names with `sep`.
+// jw joins state names into a single string with a separator.
func jw(states []string, sep string) string {
return strings.Join(states, sep)
}
@@ -355,31 +628,127 @@ func padString(str string, length int, pad string) string {
}
}
-// cloneStates deep clones the states struct and returns a copy.
-func cloneStates(states States) States {
- ret := make(States)
+// TODO move to Resolver
+func parseStruct(states Struct) Struct {
+ // TODO capitalize states
+
+ parsedStates := CloneStates(states)
for name, state := range states {
- stateCopy := State{
- Auto: state.Auto,
- Multi: state.Multi,
+
+ // avoid self removal
+ if slices.Contains(state.Remove, name) {
+ state.Remove = slicesWithout(state.Remove, name)
}
- if state.Require != nil {
- stateCopy.Require = make(S, len(state.Require))
- copy(stateCopy.Require, state.Require)
+
+ // don't Remove if in Add
+ for _, add := range state.Add {
+ if slices.Contains(state.Remove, add) {
+ state.Remove = slicesWithout(state.Remove, add)
+ }
}
- if state.Add != nil {
- stateCopy.Add = make(S, len(state.Add))
- copy(stateCopy.Add, state.Add)
+
+ // avoid being after itself
+ if slices.Contains(state.After, name) {
+ state.After = slicesWithout(state.After, name)
}
- if state.Remove != nil {
- stateCopy.Remove = make(S, len(state.Remove))
- copy(stateCopy.Remove, state.Remove)
+
+ parsedStates[name] = state
+ }
+
+ return parsedStates
+}
+
+// compareArgs return true if args2 is a subset of args1.
+func compareArgs(args1, args2 A) bool {
+ match := true
+
+ for k, v := range args2 {
+ // TODO better comparisons
+ if args1[k] != v {
+ match = false
+ break
}
- if state.After != nil {
- stateCopy.After = make(S, len(state.After))
- copy(stateCopy.After, state.After)
+ }
+
+ return match
+}
+
+func truncateStr(s string, maxLength int) string {
+ if len(s) <= maxLength {
+ return s
+ }
+ return s[:maxLength-3] + "..."
+}
+
+type handlerCall struct {
+ fn reflect.Value
+ event *Event
+ timeout bool
+}
+
+func randID() string {
+ id := make([]byte, 16)
+ _, err := rand.Read(id)
+ if err != nil {
+ return "error"
+ }
+
+ return hex.EncodeToString(id)
+}
+
+func slicesWithout[S ~[]E, E comparable](coll S, el E) S {
+ idx := slices.Index(coll, el)
+ ret := slices.Clone(coll)
+ if idx == -1 {
+ return ret
+ }
+ return slices.Delete(ret, idx, idx+1)
+}
+
+// slicesNone returns true if none of the elements of coll2 are in coll1.
+func slicesNone[S1 ~[]E, S2 ~[]E, E comparable](col1 S1, col2 S2) bool {
+ for _, el := range col2 {
+ if slices.Contains(col1, el) {
+ return false
+ }
+ }
+ return true
+}
+
+// slicesEvery returns true if all elements of coll2 are in coll1.
+func slicesEvery[S1 ~[]E, S2 ~[]E, E comparable](col1 S1, col2 S2) bool {
+ for _, el := range col2 {
+ if !slices.Contains(col1, el) {
+ return false
+ }
+ }
+ return true
+}
+
+func slicesFilter[S ~[]E, E any](coll S, fn func(item E, i int) bool) S {
+ var ret S
+ for i, el := range coll {
+ if fn(el, i) {
+ ret = append(ret, el)
+ }
+ }
+ return ret
+}
+
+func slicesReverse[S ~[]E, E any](coll S) S {
+ ret := make(S, len(coll))
+ for i := range coll {
+ ret[i] = coll[len(coll)-1-i]
+ }
+ return ret
+}
+
+func slicesUniq[S ~[]E, E comparable](coll S) S {
+ var ret S
+ for _, el := range coll {
+ if !slices.Contains(ret, el) {
+ ret = append(ret, el)
}
- ret[name] = stateCopy
}
return ret
}
diff --git a/pkg/machine/popup_test.go b/pkg/machine/popup_test.go
index 92523cc..99e2d90 100644
--- a/pkg/machine/popup_test.go
+++ b/pkg/machine/popup_test.go
@@ -1,4 +1,4 @@
-package machine_test
+package machine
import (
"context"
@@ -6,34 +6,26 @@ import (
"time"
"github.com/stretchr/testify/assert"
-
- am "github.com/pancsta/asyncmachine-go/pkg/machine"
)
-func NewPopupMachine(ctx context.Context) *am.Machine {
- return am.New(ctx, am.States{
- "Enabled": {},
- "ButtonClicked": {
- Require: am.S{"Enabled"},
- },
- "ShowingDialog": {
- Remove: am.S{"DialogVisible"},
- },
+func NewPopupMachine(ctx context.Context) *Machine {
+ return New(ctx, Struct{
+ "Enabled": {},
+ "ButtonClicked": {Require: S{"Enabled"}},
+ "ShowingDialog": {Remove: S{"DialogVisible"}},
"DownloadingData": {
Auto: true,
- Require: am.S{"ShowingDialog"},
- Remove: am.S{"DataDownloaded"},
+ Require: S{"ShowingDialog"},
+ Remove: S{"DataDownloaded"},
},
"PreloaderVisible": {
Auto: true,
- Require: am.S{"DownloadingData"},
- },
- "DataDownloaded": {
- Remove: am.S{"DownloadingData"},
+ Require: S{"DownloadingData"},
},
+ "DataDownloaded": {Remove: S{"DownloadingData"}},
"DialogVisible": {
- Require: am.S{"DataDownloaded"},
- Remove: am.S{"ShowingDialog"},
+ Require: S{"DataDownloaded"},
+ Remove: S{"ShowingDialog"},
},
}, nil)
}
@@ -43,28 +35,28 @@ type PopupMachineHandlers struct {
name string
}
-func (pm *PopupMachineHandlers) ButtonClickedState(e *am.Event) {
+func (pm *PopupMachineHandlers) ButtonClickedState(e *Event) {
// args definition
pm.name = e.Args["button"].(string)
// this will get queued
- e.Machine.Add(am.S{"ShowingDialog"}, nil)
+ e.Machine.Add(S{"ShowingDialog"}, nil)
// this will get queued later
- e.Machine.Remove(am.S{"ButtonClicked"}, nil)
+ e.Machine.Remove(S{"ButtonClicked"}, nil)
}
-func (pm *PopupMachineHandlers) DialogVisibleState(e *am.Event) {
+func (pm *PopupMachineHandlers) DialogVisibleState(e *Event) {
// data is guaranteed by the ButtonClicked state
e.Machine.Log("THE END for " + pm.name)
}
-func (pm *PopupMachineHandlers) PreloaderVisibleState(e *am.Event) {
+func (pm *PopupMachineHandlers) PreloaderVisibleState(e *Event) {
e.Machine.Log("preloader show")
}
-func (pm *PopupMachineHandlers) DownloadingDataState(e *am.Event) {
+func (pm *PopupMachineHandlers) DownloadingDataState(e *Event) {
// get the cancel context
- stateCtx := e.Machine.GetStateCtx("DownloadingData")
+ stateCtx := e.Machine.NewStateCtx("DownloadingData")
// dont block
go func() {
e.Machine.Log("fetchData start")
@@ -78,15 +70,15 @@ func (pm *PopupMachineHandlers) DownloadingDataState(e *am.Event) {
}
// data accepted
// async action finished successfully, transition to DataDownloaded
- e.Machine.Add(am.S{"DataDownloaded"}, nil)
+ e.Machine.Add(S{"DataDownloaded"}, nil)
}()
}
-func (pm *PopupMachineHandlers) DataDownloadedState(e *am.Event) {
- e.Machine.Add(am.S{"DialogVisible"}, nil)
+func (pm *PopupMachineHandlers) DataDownloadedState(e *Event) {
+ e.Machine.Add(S{"DialogVisible"}, nil)
}
-func (pm *PopupMachineHandlers) PreloaderVisibleEnd(e *am.Event) {
+func (pm *PopupMachineHandlers) PreloaderVisibleEnd(e *Event) {
e.Machine.Log("preloader hide")
}
@@ -94,11 +86,11 @@ func TestPopupMachine(t *testing.T) {
// create a new machine (with logging)
machine := NewPopupMachine(context.Background())
defer machine.Dispose()
- machine.SetLogLevel(am.LogChanges)
- // machine.SetLogLevel(am.LogOps)
- // machine.SetLogLevel(am.LogDecisions)
- // machine.SetLogLevel(am.LogEverything)
- machine.SetLogger(func(_ am.LogLevel, msg string, args ...any) {
+ machine.SetLogLevel(LogChanges)
+ // machine.SetLogLevel(LogOps)
+ // machine.SetLogLevel(LogDecisions)
+ // machine.SetLogLevel(LogEverything)
+ machine.SetLogger(func(_ LogLevel, msg string, args ...any) {
t.Logf(msg, args...)
})
// bind the Transition handlers
@@ -106,11 +98,13 @@ func TestPopupMachine(t *testing.T) {
assert.NoError(t, err)
// test
+ // TODO add timing, duplicate input events and assert correct handing of
+ // edge cases
// start accepting input
- machine.Add(am.S{"Enabled"}, nil)
+ machine.Add(S{"Enabled"}, nil)
// external action triggers the popup workflow
- machine.Add(am.S{"ButtonClicked"}, am.A{"button": "red"})
+ machine.Add(S{"ButtonClicked"}, A{"button": "red"})
// wait for DialogVisible
<-machine.When(S{"DialogVisible"}, nil)
diff --git a/pkg/machine/resolver.go b/pkg/machine/resolver.go
index f60e663..e3aa1c1 100644
--- a/pkg/machine/resolver.go
+++ b/pkg/machine/resolver.go
@@ -1,13 +1,14 @@
package machine
import (
+ "fmt"
+ "slices"
"sort"
-
- "github.com/samber/lo"
)
// RelationsResolver is an interface for parsing relations between states.
-// TODO support custom relation types
+// Not thread-safe.
+// TODO support custom relation types and additional state properties.
type RelationsResolver interface {
// GetTargetStates returns the target states after parsing the relations.
GetTargetStates(t *Transition, calledStates S) S
@@ -17,15 +18,22 @@ type RelationsResolver interface {
// SortStates sorts the states in the order their handlers should be
// executed.
SortStates(states S)
+ // GetRelationsOf returns a list of relation types of the given state.
+ GetRelationsOf(fromState string) ([]Relation, error)
+ // GetRelationsBetween returns a list of relation types between the given
+ // states.
+ GetRelationsBetween(fromState, toState string) ([]Relation, error)
}
// DefaultRelationsResolver is the default implementation of the
-// RelationsResolver.
+// RelationsResolver with Add, Remove, Require and After. It can be overridden
+// using Opts.Resolver.
type DefaultRelationsResolver struct {
Machine *Machine
Transition *Transition
}
+// GetTargetStates implements RelationsResolver.GetTargetStates.
func (rr *DefaultRelationsResolver) GetTargetStates(
t *Transition, calledStates S,
) S {
@@ -34,72 +42,83 @@ func (rr *DefaultRelationsResolver) GetTargetStates(
m := t.Machine
calledStates = m.MustParseStates(calledStates)
calledStates = rr.parseAdd(calledStates)
- calledStates = lo.Uniq(calledStates)
+ calledStates = slicesUniq(calledStates)
calledStates = rr.parseRequire(calledStates)
+
// start from the end
- resolvedS := lo.Reverse(calledStates)
+ resolvedS := slicesReverse(calledStates)
+
// collect blocked calledStates
alreadyBlocked := S{}
+
// remove already blocked calledStates
- resolvedS = lo.Filter(resolvedS, func(name string, _ int) bool {
+ resolvedS = slicesFilter(resolvedS, func(name string, _ int) bool {
blockedBy := rr.stateBlockedBy(calledStates, name)
+
// ignore blocking by already blocked states
- blockedBy = lo.Filter(blockedBy, func(blockerName string, _ int) bool {
- return !lo.Contains(alreadyBlocked, blockerName)
+ blockedBy = slicesFilter(blockedBy, func(blockerName string, _ int) bool {
+ return !slices.Contains(alreadyBlocked, blockerName)
})
if len(blockedBy) == 0 {
return true
}
+
alreadyBlocked = append(alreadyBlocked, name)
// if state wasn't implied by another state (was one of the active
// states) then make it a higher priority log msg
var lvl LogLevel
- if m.Is(S{name}) {
+ if m.is(S{name}) {
lvl = LogOps
} else {
lvl = LogDecisions
}
+
m.log(lvl, "[rel:remove] %s by %s", name, j(blockedBy))
- if m.Is(S{name}) {
- t.addSteps(newStep("", name, TransitionStepTypeRemove, nil))
+ if m.is(S{name}) {
+ t.addSteps(newStep("", name, StepRemove, nil))
} else {
- t.addSteps(newStep("", name, TransitionStepTypeNoSet, nil))
+ t.addSteps(newStep("", name, StepRemoveNotActive, nil))
}
+
return false
})
+
// states removed by the states which are about to be set
- toRemove := lo.Reduce(resolvedS, func(acc S, name string, _ int) S {
- state := m.States[name]
+ var toRemove S
+ for _, name := range resolvedS {
+ state := m.states[name]
if state.Remove != nil {
- acc = append(acc, state.Remove...)
+ toRemove = append(toRemove, state.Remove...)
}
- return acc
- }, S{})
- resolvedS = lo.Filter(rr.parseAdd(resolvedS), func(name string,
+ }
+ resolvedS = slicesFilter(rr.parseAdd(resolvedS), func(name string,
_ int,
) bool {
- return !lo.Contains(toRemove, name)
+ return !slices.Contains(toRemove, name)
})
- resolvedS = lo.Uniq(resolvedS)
+ resolvedS = slicesUniq(resolvedS)
+
// Parsing required states allows to avoid cross-removal of states
- targetStates := rr.parseRequire(lo.Reverse(resolvedS))
+ targetStates := rr.parseRequire(slicesReverse(resolvedS))
rr.SortStates(targetStates)
+
return targetStates
}
+// GetAutoMutation implements RelationsResolver.GetAutoMutation.
func (rr *DefaultRelationsResolver) GetAutoMutation() *Mutation {
t := rr.Transition
m := t.Machine
var toAdd []string
// check all Auto states
- for s := range m.States {
- if !m.States[s].Auto {
+ for s := range m.states {
+ if !m.states[s].Auto {
continue
}
// check if the state is blocked by another active state
isBlocked := func() bool {
- for _, active := range m.ActiveStates {
- if lo.Contains(m.States[active].Remove, s) {
+ for _, active := range m.activeStates {
+ if slices.Contains(m.states[active].Remove, s) {
m.log(LogEverything, "[auto:rel:remove] %s by %s", s,
active)
return true
@@ -107,7 +126,7 @@ func (rr *DefaultRelationsResolver) GetAutoMutation() *Mutation {
}
return false
}
- if !m.Is(S{s}) && !isBlocked() {
+ if !m.is(S{s}) && !isBlocked() {
toAdd = append(toAdd, s)
}
}
@@ -115,33 +134,35 @@ func (rr *DefaultRelationsResolver) GetAutoMutation() *Mutation {
return nil
}
return &Mutation{
- Type: MutationTypeAdd,
+ Type: MutationAdd,
CalledStates: toAdd,
Auto: true,
}
}
+// SortStates implements RelationsResolver.SortStates.
func (rr *DefaultRelationsResolver) SortStates(states S) {
t := rr.Transition
- m := t.Machine
+ m := rr.Machine
sort.SliceStable(states, func(i, j int) bool {
name1 := states[i]
name2 := states[j]
- state1 := m.States[name1]
- state2 := m.States[name2]
- if lo.Contains(state1.After, name2) {
+ state1 := m.states[name1]
+ state2 := m.states[name2]
+ if slices.Contains(state1.After, name2) {
t.addSteps(newStep(name2, name1,
- TransitionStepTypeRelation, RelationAfter))
+ StepRelation, RelationAfter))
return false
- } else if lo.Contains(state2.After, name1) {
+ } else if slices.Contains(state2.After, name1) {
t.addSteps(newStep(name1, name2,
- TransitionStepTypeRelation, RelationAfter))
+ StepRelation, RelationAfter))
return true
}
return false
})
}
+// TODO docs
func (rr *DefaultRelationsResolver) parseAdd(states S) S {
t := rr.Transition
ret := states
@@ -150,16 +171,18 @@ func (rr *DefaultRelationsResolver) parseAdd(states S) S {
for changed {
changed = false
for _, name := range states {
- if lo.Contains(t.StatesBefore, name) {
+ state := rr.Machine.states[name]
+
+ if slices.Contains(t.StatesBefore, name) && !state.Multi {
continue
}
- state := t.Machine.States[name]
- if lo.Contains(visited, name) || state.Add == nil {
+ if slices.Contains(visited, name) || state.Add == nil {
continue
}
- t.addSteps(newSteps(name, state.Add, TransitionStepTypeRelation,
+
+ t.addSteps(newSteps(name, state.Add, StepRelation,
RelationAdd)...)
- t.addSteps(newSteps("", state.Add, TransitionStepTypeSet, nil)...)
+ t.addSteps(newSteps("", state.Add, StepSet, nil)...)
ret = append(ret, state.Add...)
visited = append(visited, name)
changed = true
@@ -168,33 +191,39 @@ func (rr *DefaultRelationsResolver) parseAdd(states S) S {
return ret
}
+// TODO docs
func (rr *DefaultRelationsResolver) stateBlockedBy(
blockingStates S, blocked string,
) S {
m := rr.Machine
t := rr.Transition
blockedBy := S{}
+
for _, blocking := range blockingStates {
- state := m.States[blocking]
- if !lo.Contains(state.Remove, blocked) {
+ state := m.states[blocking]
+ if !slices.Contains(state.Remove, blocked) {
continue
}
- t.addSteps(newStep(blocking, blocked, TransitionStepTypeRelation,
+
+ t.addSteps(newStep(blocking, blocked, StepRelation,
RelationRemove))
blockedBy = append(blockedBy, blocking)
}
+
return blockedBy
}
+// TODO docs
func (rr *DefaultRelationsResolver) parseRequire(states S) S {
t := rr.Transition
lengthBefore := 0
// maps of states with their required states missing
missingMap := map[string]S{}
+
for lengthBefore != len(states) {
lengthBefore = len(states)
- states = lo.Filter(states, func(name string, _ int) bool {
- state := t.Machine.States[name]
+ states = slicesFilter(states, func(name string, _ int) bool {
+ state := rr.Machine.states[name]
missingReqs := rr.getMissingRequires(name, state, states)
if len(missingReqs) > 0 {
missingMap[name] = missingReqs
@@ -202,6 +231,7 @@ func (rr *DefaultRelationsResolver) parseRequire(states S) S {
return len(missingReqs) == 0
})
}
+
if len(missingMap) > 0 {
names := S{}
for state, notFound := range missingMap {
@@ -213,26 +243,98 @@ func (rr *DefaultRelationsResolver) parseRequire(states S) S {
t.Machine.log(LogOps, "[reject] %s", jw(names, " "))
}
}
+
return states
}
+// TODO docs
func (rr *DefaultRelationsResolver) getMissingRequires(
name string, state State, states S,
) S {
t := rr.Transition
ret := S{}
+
for _, req := range state.Require {
- t.addSteps(newStep(name, req, TransitionStepTypeRelation,
+ t.addSteps(newStep(name, req, StepRelation,
RelationRequire))
- if lo.Contains(states, req) {
+ if slices.Contains(states, req) {
continue
}
ret = append(ret, req)
- t.addSteps(newStep(name, "", TransitionStepTypeNoSet, nil))
- if lo.Contains(t.Mutation.CalledStates, name) {
+ t.addSteps(newStep(name, "", StepRemoveNotActive, nil))
+ if slices.Contains(t.Mutation.CalledStates, name) {
t.addSteps(newStep("", req,
- TransitionStepTypeCancel, nil))
+ StepCancel, nil))
}
}
+
return ret
}
+
+// GetRelationsBetween returns a list of relation types between the given
+// states. Not thread safe.
+func (rr *DefaultRelationsResolver) GetRelationsBetween(
+ fromState, toState string,
+) ([]Relation, error) {
+ m := rr.Machine
+
+ if !m.Has1(fromState) {
+ return nil, fmt.Errorf("%w: %s", ErrStateUnknown, fromState)
+ }
+ if !m.Has1(toState) {
+ return nil, fmt.Errorf("%w: %s", ErrStateUnknown, toState)
+ }
+
+ state := m.states[fromState]
+ var relations []Relation
+
+ if state.Add != nil && slices.Contains(state.Add, toState) {
+ relations = append(relations, RelationAdd)
+ }
+
+ if state.Require != nil && slices.Contains(state.Require, toState) {
+ relations = append(relations, RelationRequire)
+ }
+
+ if state.Remove != nil && slices.Contains(state.Remove, toState) {
+ relations = append(relations, RelationRemove)
+ }
+
+ if state.After != nil && slices.Contains(state.After, toState) {
+ relations = append(relations, RelationAfter)
+ }
+
+ return relations, nil
+}
+
+// GetRelationsOf returns a list of relation types of the given state.
+// Not thread safe.
+func (rr *DefaultRelationsResolver) GetRelationsOf(fromState string) (
+ []Relation, error,
+) {
+ m := rr.Machine
+
+ if !m.Has1(fromState) {
+ return nil, fmt.Errorf("%w: %s", ErrStateUnknown, fromState)
+ }
+ state := m.states[fromState]
+
+ var relations []Relation
+ if state.Add != nil {
+ relations = append(relations, RelationAdd)
+ }
+
+ if state.Require != nil {
+ relations = append(relations, RelationRequire)
+ }
+
+ if state.Remove != nil {
+ relations = append(relations, RelationRemove)
+ }
+
+ if state.After != nil {
+ relations = append(relations, RelationAfter)
+ }
+
+ return relations, nil
+}
diff --git a/pkg/machine/transition.go b/pkg/machine/transition.go
index 46ea786..3edf1ba 100644
--- a/pkg/machine/transition.go
+++ b/pkg/machine/transition.go
@@ -1,17 +1,14 @@
package machine
import (
+ "slices"
"strings"
-
- "github.com/google/uuid"
-
- "github.com/samber/lo"
)
-func newStep(from string, to string, stepType TransitionStepType,
+func newStep(from string, to string, stepType StepType,
data any,
-) *TransitionStep {
- return &TransitionStep{
+) *Step {
+ return &Step{
FromState: from,
ToState: to,
Type: stepType,
@@ -19,10 +16,10 @@ func newStep(from string, to string, stepType TransitionStepType,
}
}
-func newSteps(from string, toStates S, stepType TransitionStepType,
+func newSteps(from string, toStates S, stepType StepType,
data any,
-) []*TransitionStep {
- var ret []*TransitionStep
+) []*Step {
+ var ret []*Step
for _, to := range toStates {
ret = append(ret, newStep(from, to, stepType, data))
}
@@ -33,16 +30,20 @@ func newSteps(from string, toStates S, stepType TransitionStepType,
type Transition struct {
ID string
// List of steps taken by this transition (so far).
- Steps []*TransitionStep
+ Steps []*Step
// When true, execution of the transition has been completed.
IsCompleted bool
// states before the transition
StatesBefore S
// clocks of the states from before the transition
+ // TODO timeBefore, produce Clocks via ClockBefore(), add index diffs
ClocksBefore Clocks
- // States with "enter" handlers to execute
+ // clocks of the states from after the transition
+ // TODO timeAfter, produce Clocks via ClockAfter(), add index diffs
+ ClocksAfter Clocks
+ // Struct with "enter" handlers to execute
Enters S
- // States with "exit" handlers to executed
+ // Struct with "exit" handlers to executed
Exits S
// target states after parsing the relations
TargetStates S
@@ -54,9 +55,10 @@ type Transition struct {
Machine *Machine
// Log entries produced during the transition
LogEntries []string
+ // start time of the transition
// Latest / current step of the transition
- latestStep *TransitionStep
+ latestStep *Step
}
// newTransition creates a new transition for the given mutation.
@@ -65,52 +67,70 @@ func newTransition(m *Machine, item *Mutation) *Transition {
defer m.activeStatesLock.RUnlock()
clocks := Clocks{}
- for _, state := range m.ActiveStates {
+ for _, state := range m.StateNames {
clocks[state] = m.clock[state]
}
+
t := &Transition{
- ID: uuid.New().String(),
+ ID: randID(),
Mutation: item,
- StatesBefore: m.ActiveStates,
+ StatesBefore: m.activeStates,
ClocksBefore: clocks,
Machine: m,
Accepted: true,
}
- // set early to catch the logs
+
+ // assign early to catch the logs
m.Transition = t
+
+ // tracers
+ for i := range t.Machine.Tracers {
+ t.Machine.Tracers[i].TransitionInit(t)
+ m.emit(EventTransitionInit, nil, nil)
+ }
+
+ // log
states := t.CalledStates()
mutType := t.Type()
- t.addSteps(newSteps("", states, TransitionStepTypeRequested, nil)...)
+ logArgs := t.LogArgs()
+ t.addSteps(newSteps("", states, StepRequested, nil)...)
+
if item.Auto {
- m.log(LogDecisions, "[%s:auto] %s", mutType, j(states))
+ m.log(LogDecisions, "[%s:auto] %s%s", mutType, j(states), logArgs)
} else {
- m.log(LogOps, "[%s] %s", mutType, j(states))
+ m.log(LogOps, "[%s] %s%s", mutType, j(states), logArgs)
}
+
statesToSet := S{}
switch mutType {
- case MutationTypeRemove:
- statesToSet = lo.Filter(m.ActiveStates, func(state string, _ int) bool {
- return !lo.Contains(states, state)
+
+ case MutationRemove:
+ statesToSet = slicesFilter(m.activeStates, func(state string, _ int) bool {
+ return !slices.Contains(states, state)
})
- t.addSteps(newSteps("", states, TransitionStepTypeRemove, nil)...)
- case MutationTypeAdd:
+ t.addSteps(newSteps("", states, StepRemove, nil)...)
+
+ case MutationAdd:
statesToSet = append(statesToSet, states...)
- statesToSet = append(statesToSet, m.ActiveStates...)
- t.addSteps(newSteps("", DiffStates(statesToSet, m.ActiveStates),
- TransitionStepTypeSet, nil)...)
- case MutationTypeSet:
+ statesToSet = append(statesToSet, m.activeStates...)
+ t.addSteps(newSteps("", DiffStates(statesToSet, m.activeStates),
+ StepSet, nil)...)
+
+ case MutationSet:
statesToSet = states
- t.addSteps(newSteps("", DiffStates(statesToSet, m.ActiveStates),
- TransitionStepTypeSet, nil)...)
- t.addSteps(newSteps("", DiffStates(m.ActiveStates, statesToSet),
- TransitionStepTypeRemove, nil)...)
+ t.addSteps(newSteps("", DiffStates(statesToSet, m.activeStates),
+ StepSet, nil)...)
+ t.addSteps(newSteps("", DiffStates(m.activeStates, statesToSet),
+ StepRemove, nil)...)
}
+
t.TargetStates = m.Resolver.GetTargetStates(t, statesToSet)
impliedStates := DiffStates(t.TargetStates, statesToSet)
if len(impliedStates) > 0 {
m.log(LogOps, "[implied] %s", j(impliedStates))
}
+
t.setupAccepted()
if t.Accepted {
t.setupExitEnter()
@@ -130,7 +150,8 @@ func (t *Transition) CalledStates() S {
return t.Mutation.CalledStates
}
-// Args returns the argument object passed to the mutation method (if any).
+// Args returns the argument map passed to the mutation method
+// (or an empty one).
func (t *Transition) Args() A {
return t.Mutation.Args
}
@@ -140,7 +161,26 @@ func (t *Transition) Type() MutationType {
return t.Mutation.Type
}
-func (t *Transition) addSteps(steps ...*TransitionStep) {
+// LogArgs returns a text snippet with arguments which should be logged for this
+// Mutation.
+func (t *Transition) LogArgs() string {
+ matcher := t.Machine.GetLogArgs()
+ if matcher == nil {
+ return ""
+ }
+
+ var args []string
+ for k, v := range matcher(t.Mutation.Args) {
+ args = append(args, k+"="+v)
+ }
+ if len(args) == 0 {
+ return ""
+ }
+
+ return " (" + strings.Join(args, " ") + ")"
+}
+
+func (t *Transition) addSteps(steps ...*Step) {
t.Steps = append(t.Steps, steps...)
}
@@ -149,11 +189,13 @@ func (t *Transition) addSteps(steps ...*TransitionStep) {
func (t *Transition) String() string {
var lines []string
for k := range t.Steps {
+
touch := t.Steps[k]
line := ""
if touch.ToState != "" {
line += touch.ToState + " -> "
}
+
line += touch.FromState
if touch.Type > 0 {
line += touch.Type.String()
@@ -161,27 +203,30 @@ func (t *Transition) String() string {
if touch.Data != nil {
line += " (" + touch.Data.(string) + ")"
}
+
lines = append(lines, line)
}
+
return strings.Join(lines, "\n")
}
func (t *Transition) setupExitEnter() {
m := t.Machine
+
// collect the exit handlers
- exits := DiffStates(m.ActiveStates, t.TargetStates)
+ exits := DiffStates(m.activeStates, t.TargetStates)
m.Resolver.SortStates(exits)
+
// collect the enters handlers
- enters := S{}
+ var enters S
for _, s := range t.TargetStates {
// enter activate state only for multi states called directly
- // not implied by Add
- // TODO m.is()
- if m.Is(S{s}) && !(m.States[s].Multi && lo.Contains(t.CalledStates(), s)) {
- continue
+ state := m.states[s]
+ if !m.is(S{s}) || (state.Multi && slices.Contains(t.CalledStates(), s)) {
+ enters = append(enters, s)
}
- enters = append(enters, s)
}
+
// save
t.Exits = exits
t.Enters = enters
@@ -197,7 +242,7 @@ func (t *Transition) emitSelfEvents() Result {
continue
}
name := s + s
- step := newStep(s, "", TransitionStepTypeTransition, name)
+ step := newStep(s, "", StepHandler, name)
step.IsSelf = true
ret, handlerCalled = m.emit(name, t.Mutation.Args, step)
if handlerCalled {
@@ -212,18 +257,23 @@ func (t *Transition) emitSelfEvents() Result {
func (t *Transition) emitEnterEvents() Result {
for _, toState := range t.Enters {
+
+ // TODO double check removal
// states implied by Add cant cancel the transition
- isCalled := lo.Contains(t.CalledStates(), toState)
+ // isCalled := slices.Contains(t.CalledStates(), toState)
args := t.Mutation.Args
+
ret := t.emitHandler("Any", toState, "Any"+toState, args)
- if ret == Canceled && isCalled {
+ if ret == Canceled {
return ret
}
+
ret = t.emitHandler("", toState, toState+"Enter", args)
- if ret == Canceled && isCalled {
+ if ret == Canceled {
return ret
}
}
+
return Executed
}
@@ -249,7 +299,7 @@ func (t *Transition) emitExitEvents() Result {
}
func (t *Transition) emitHandler(from, to, event string, args A) Result {
- step := newStep(from, to, TransitionStepTypeTransition, event)
+ step := newStep(from, to, StepHandler, event)
t.latestStep = step
ret, handlerCalled := t.Machine.emit(event, args, step)
if handlerCalled {
@@ -262,22 +312,27 @@ func (t *Transition) emitFinalEvents() {
finals := S{}
finals = append(finals, t.Exits...)
finals = append(finals, t.Enters...)
+
for _, s := range finals {
- isEnter := lo.Contains(t.Enters, s)
+ isEnter := slices.Contains(t.Enters, s)
+
var handler string
if isEnter {
handler = s + "State"
} else {
handler = s + "End"
}
- step := newStep(s, "", TransitionStepTypeTransition, handler)
+
+ step := newStep(s, "", StepHandler, handler)
step.IsFinal = true
step.IsEnter = isEnter
t.latestStep = step
ret, handlerCalled := t.Machine.emit(handler, t.Mutation.Args, step)
+
if handlerCalled {
t.addSteps(step)
}
+
if ret == Canceled {
break
}
@@ -299,13 +354,15 @@ func (t *Transition) emitEvents() Result {
// NEGOTIATION CALLS PHASE (cancellable)
// FooFoo handlers
- if result != Canceled && t.Type() != MutationTypeRemove {
+ if result != Canceled && t.Type() != MutationRemove {
result = t.emitSelfEvents()
}
+
// FooExit handlers
if result != Canceled {
result = t.emitExitEvents()
}
+
// FooEnter handlers
if result != Canceled {
result = t.emitEnterEvents()
@@ -313,24 +370,37 @@ func (t *Transition) emitEvents() Result {
// FINAL HANDLERS (non cancellable)
if result != Canceled {
+
m.setActiveStates(t.CalledStates(), t.TargetStates, t.IsAuto())
t.emitFinalEvents()
t.IsCompleted = true
- hasStateChanged = m.HasStateChanged(t.StatesBefore, t.ClocksBefore)
+
+ hasStateChanged = m.HasStateChangedSince(t.ClocksBefore)
+
if hasStateChanged {
m.emit(EventTick, A{"before": t.StatesBefore}, nil)
}
}
+ // gather new clock values
+ clocks := Clocks{}
+ for _, state := range m.StateNames {
+ clocks[state] = m.clock[state]
+ }
+ t.ClocksAfter = clocks
+
// AUTO STATES
if result == Canceled {
+
+ t.Accepted = false
m.emit(EventTransitionCancel, txArgs, nil)
} else if hasStateChanged && !t.IsAuto() {
+
autoMutation := m.Resolver.GetAutoMutation()
if autoMutation != nil {
- m.log(LogOps, "[auto] %s", j((*autoMutation).CalledStates))
+ m.log(LogOps, "[auto] %s", j(autoMutation.CalledStates))
// unshift
- m.Queue = append([]Mutation{*autoMutation}, m.Queue...)
+ m.queue = append([]*Mutation{autoMutation}, m.queue...)
}
}
@@ -338,15 +408,19 @@ func (t *Transition) emitEvents() Result {
m.logEntriesLock.Lock()
// TODO struct type
txArgs["pre_logs"] = m.logEntries
- txArgs["queue_len"] = len(m.Queue)
+ txArgs["queue_len"] = len(m.queue)
m.logEntries = []string{}
m.logEntriesLock.Unlock()
m.emit(EventTransitionEnd, txArgs, nil)
+ for i := range t.Machine.Tracers {
+ t.Machine.Tracers[i].TransitionEnd(t)
+ }
+
if result == Canceled {
return Canceled
}
- if t.Type() == MutationTypeRemove {
+ if t.Type() == MutationRemove {
if m.Not(t.CalledStates()) {
return Executed
} else {
@@ -366,7 +440,7 @@ func (t *Transition) emitEvents() Result {
func (t *Transition) setupAccepted() {
m := t.Machine
// Dropping states doesn't require an acceptance
- if t.Type() == MutationTypeRemove {
+ if t.Type() == MutationRemove {
return
}
notAccepted := DiffStates(t.CalledStates(), t.TargetStates)
@@ -384,5 +458,5 @@ func (t *Transition) setupAccepted() {
}
t.Accepted = false
m.log(LogOps, "[cancel:reject] %s", j(notAccepted))
- t.addSteps(newSteps("", notAccepted, TransitionStepTypeCancel, nil)...)
+ t.addSteps(newSteps("", notAccepted, StepCancel, nil)...)
}
diff --git a/pkg/machine/utils_test.go b/pkg/machine/utils_test.go
index b2d699f..80e990d 100644
--- a/pkg/machine/utils_test.go
+++ b/pkg/machine/utils_test.go
@@ -1,66 +1,85 @@
-package machine_test
+package machine
import (
"fmt"
"os"
"strings"
+ "sync"
"testing"
"github.com/lithammer/dedent"
"github.com/stretchr/testify/assert"
-
- am "github.com/pancsta/asyncmachine-go/pkg/machine"
)
type History struct {
- Order []string
- Counter map[string]int
+ Order []string
+ Counter map[string]int
+ CounterMx sync.Mutex
}
-func trackTransitions(m *am.Machine, events S) *History {
+func trackTransitions(m *Machine, events S) *History {
history := &History{
Order: []string{},
Counter: map[string]int{},
}
isBound := make(chan bool)
- ch := m.On(append(events, "queue-end"), nil)
+ ch := m.OnEvent(append(events, EventQueueEnd), nil)
go func() {
// guaranteed to run after listening starts
go func() {
// causes a return from trackTransitions
close(isBound)
}()
- for {
- e, ok := <-ch
- if !ok {
- break
- }
- if e.Name == "queue-end" {
+
+ for e := range ch {
+ if e.Name == EventQueueEnd {
continue
}
history.Order = append(history.Order, e.Name)
+
+ history.CounterMx.Lock()
if _, ok := history.Counter[e.Name]; !ok {
history.Counter[e.Name] = 0
}
history.Counter[e.Name]++
+
+ history.CounterMx.Unlock()
}
}()
+
<-isBound
return history
}
-func assertStates(t *testing.T, m *am.Machine, expected S,
+func assertStates(t *testing.T, m *Machine, expected S,
msgAndArgs ...interface{},
) {
- assert.ElementsMatch(t, expected, m.ActiveStates, msgAndArgs...)
+ assert.ElementsMatch(t, expected, m.activeStates, msgAndArgs...)
}
-func assertNoException(t *testing.T, m *am.Machine) {
- assert.False(t, m.Is(S{"Exception"}), "Exception state active")
+func assertTimes(t *testing.T, m *Machine, states S, times T,
+ msgAndArgs ...interface{},
+) {
+ assert.Equal(t, m.Time(states), times, msgAndArgs...)
+}
+
+func assertClocks(t *testing.T, m *Machine, states S, times T,
+ msgAndArgs ...interface{},
+) {
+ for i, state := range states {
+ assert.True(t, m.IsClock(state, times[i]), msgAndArgs...)
+ }
+}
+
+func assertNoException(t *testing.T, m *Machine) {
+ assert.False(t, m.Is1(Exception), "Exception state active")
}
func assertEventCounts(t *testing.T, history *History, expected int) {
+ history.CounterMx.Lock()
+ defer history.CounterMx.Unlock()
+
// assert event counts
for _, count := range history.Counter {
assert.Equal(t, expected, count)
@@ -68,15 +87,18 @@ func assertEventCounts(t *testing.T, history *History, expected int) {
}
func assertEventCountsMin(t *testing.T, history *History, expected int) {
+ history.CounterMx.Lock()
+ defer history.CounterMx.Unlock()
+
// assert event counts
for event, count := range history.Counter {
assert.GreaterOrEqual(t, expected, count, "event %s call count", event)
}
}
-func captureLog(t *testing.T, m *am.Machine, log *string) {
- m.SetLogLevel(am.LogEverything)
- m.SetLogger(func(i am.LogLevel, msg string, args ...any) {
+func captureLog(t *testing.T, m *Machine, log *string) {
+ m.SetLogLevel(LogEverything)
+ m.SetLogger(func(i LogLevel, msg string, args ...any) {
if os.Getenv("AM_DEBUG") != "" {
t.Logf(msg, args...)
}
@@ -84,7 +106,7 @@ func captureLog(t *testing.T, m *am.Machine, log *string) {
})
}
-func assertString(t *testing.T, m *am.Machine, expected string, states S) {
+func assertString(t *testing.T, m *Machine, expected string, states S) {
assert.Equal(t,
strings.Trim(dedent.Dedent(expected), "\n"),
strings.Trim(m.Inspect(states), "\n"))
diff --git a/pkg/telemetry/README.md b/pkg/telemetry/README.md
new file mode 100644
index 0000000..beafb7b
--- /dev/null
+++ b/pkg/telemetry/README.md
@@ -0,0 +1,37 @@
+# Telemetry
+
+![AM traces in jaeger via otel](../../assets/otel-jaeger.png)
+
+[`pkg/telemetry`](pkg/telemetry) provides various telemetry exporters.
+
+## [Open Telemetry](https://opentelemetry.io/)
+
+Otel integration exposes machine's states and transitions as Otel traces, compatible with
+[Jaeger](https://www.jaegertracing.io/). Tracers are inherited from parent machines and provide the following tree view:
+
+```text
+- mach:ID
+ - states
+ - Foo
+ - Foo (trace)
+ - Foo (trace)
+ - ...
+ - ...
+ - transitions
+ - [add] Foo
+ - FooEnter (trace)
+ - FooState (trace)
+ - ...
+ - ...
+ - submachines
+ - mach:ID2
+ - ...
+ - ...
+```
+
+See [`pkg/telemetry`](pkg/telemetry) for more info or [import an existing asset](assets/json)
+
+## am-dbg
+
+am-dbg telemetry delivers `DbgMsg` and `DbgMsgStruct` via simple `net/rpc` to `tools/cmd/am-dbg`. It can be consumed by
+a custom client as well.
diff --git a/pkg/telemetry/prometheus/README.md b/pkg/telemetry/prometheus/README.md
new file mode 100644
index 0000000..878f848
--- /dev/null
+++ b/pkg/telemetry/prometheus/README.md
@@ -0,0 +1,22 @@
+# Prometheus
+
+![prometheus grafana](../../../assets/prometheus-grafana.png)
+
+[`pkg/telemetry/prometheus`](pkg/telemetry/prometheus) binds to machine's transactions and averages the values withing
+an interval exposing various metrics. Combined with [Grafana](https://grafana.com/), it can be used to monitor the
+metrics of you machines.
+
+Metrics:
+
+- queue size
+- states: active, inactive, added, removed
+- transition duration (machine time)
+- transition duration (normal time)
+- transition's steps amount
+- exceptions count
+- registered states
+
+Grafana dashboards can be:
+
+- generated using `task gen-grafana-dashboard IDS=mach1,mach2`
+- or imported from [assets](assets/grafana-mach-sim,sim-p1.json)
diff --git a/pkg/telemetry/prometheus/prometheus.go b/pkg/telemetry/prometheus/prometheus.go
new file mode 100644
index 0000000..9312432
--- /dev/null
+++ b/pkg/telemetry/prometheus/prometheus.go
@@ -0,0 +1,453 @@
+// Package prometheus provides Prometheus metrics for asyncmachine.
+// The metrics are collected from the machine's transitions and states.
+//
+// Exported metrics:
+// - states amount
+// - relations amount
+// - rel referenced states
+package prometheus
+
+// import "github.com/pancsta/asyncmachine-go/pkg/telemetry/prometheus"
+
+import (
+ "sync"
+ "time"
+
+ "github.com/pancsta/asyncmachine-go/pkg/machine"
+ "github.com/prometheus/client_golang/prometheus"
+)
+
+// Metrics is a set of Prometheus metrics for a machine.
+type Metrics struct {
+ mx sync.Mutex
+ closed bool
+ lastUpdate time.Time
+ interval time.Duration
+
+ ////// mach definition
+
+ // number of registered states
+ StatesAmount prometheus.Gauge
+
+ // number of relations for all registered states
+ RelAmount prometheus.Gauge
+
+ // number of state referenced by relations for all registered states
+ RefStatesAmount prometheus.Gauge
+
+ ////// tx data
+
+ // current number of queued transitions (per transition)
+ QueueSize prometheus.Gauge
+ queueSize uint64
+ queueSizeLen uint
+
+ // transition duration in machine's clock (ticks per tx)
+ TxTick prometheus.Gauge
+ txTick uint64
+ txTickLen uint
+
+ // number of active states (per transition)
+ StatesActiveAmount prometheus.Gauge
+ statesActiveAmount uint64
+ statesActiveAmountLen uint
+
+ // number of inactive states (per transition)
+ StatesInactiveAmount prometheus.Gauge
+ statesInactiveAmount uint64
+ statesInactiveAmountLen uint
+
+ // number of states added (per transition)
+ StatesAdded prometheus.Gauge
+ statesAdded uint64
+ statesAddedLen uint
+
+ // number of states removed (per transition)
+ StatesRemoved prometheus.Gauge
+ statesRemoved uint64
+ statesRemovedLen uint
+
+ // number of states touched (per transition)
+ StatesTouched prometheus.Gauge
+ statesTouched uint64
+ statesTouchedLen uint
+
+ // number of errors
+ ExceptionsCount prometheus.Gauge
+ exceptionsCount uint64
+ exceptionsCountLen uint
+
+ ////// stats
+
+ // steps per transition
+ StepsAmount prometheus.Gauge
+ stepsAmount uint64
+ stepsAmountLen uint
+
+ // amount of executed handlers per tx
+ HandlersAmount prometheus.Gauge
+ handlersAmount uint64
+ handlersAmountLen uint
+
+ // transition time
+ TxTime prometheus.Gauge
+ txTime uint64
+ txTimeLen uint
+}
+
+func newMetrics(mach *machine.Machine, interval time.Duration) *Metrics {
+ machID := machine.NormalizeID(mach.ID)
+
+ return &Metrics{
+ interval: interval,
+ lastUpdate: time.Now(),
+
+ ///// mach definition
+
+ StatesAmount: prometheus.NewGauge(prometheus.GaugeOpts{
+ Name: "states_amount",
+ Help: "Number of registered states",
+ Subsystem: machID,
+ Namespace: "mach",
+ }),
+ RelAmount: prometheus.NewGauge(prometheus.GaugeOpts{
+ Name: "relations_amount",
+ Help: "Number of relations for all registered states",
+ Subsystem: machID,
+ Namespace: "mach",
+ }),
+ RefStatesAmount: prometheus.NewGauge(prometheus.GaugeOpts{
+ Name: "ref_states_amount",
+ Help: "Number of states referenced by relations for all" +
+ " registered states",
+ Subsystem: machID,
+ Namespace: "mach",
+ }),
+
+ ///// tx data
+
+ QueueSize: prometheus.NewGauge(prometheus.GaugeOpts{
+ Name: "queue_size",
+ Help: "Current number of queued transitions",
+ Subsystem: machID,
+ Namespace: "mach",
+ }),
+ TxTick: prometheus.NewGauge(prometheus.GaugeOpts{
+ Name: "tx_tick",
+ Help: "Tick size of this tx (sum of all changed clocks)",
+ Subsystem: machID,
+ Namespace: "mach",
+ }),
+ ExceptionsCount: prometheus.NewGauge(prometheus.GaugeOpts{
+ Name: "exceptions_count",
+ Help: "Number of errors",
+ Subsystem: machID,
+ Namespace: "mach",
+ }),
+ StatesAdded: prometheus.NewGauge(prometheus.GaugeOpts{
+ Name: "states_added",
+ Help: "Struct added",
+ Subsystem: machID,
+ Namespace: "mach",
+ }),
+ StatesRemoved: prometheus.NewGauge(prometheus.GaugeOpts{
+ Name: "states_removed",
+ Help: "Struct removed",
+ Subsystem: machID,
+ Namespace: "mach",
+ }),
+ StatesTouched: prometheus.NewGauge(prometheus.GaugeOpts{
+ Name: "states_touched",
+ Help: "Struct touched",
+ Subsystem: machID,
+ Namespace: "mach",
+ }),
+ StepsAmount: prometheus.NewGauge(prometheus.GaugeOpts{
+ Name: "steps_amount",
+ Help: "Steps per transition",
+ Subsystem: machID,
+ Namespace: "mach",
+ }),
+ HandlersAmount: prometheus.NewGauge(prometheus.GaugeOpts{
+ Name: "handlers_amount",
+ Help: "Amount of executed handlers per tx",
+ Subsystem: machID,
+ Namespace: "mach",
+ }),
+ TxTime: prometheus.NewGauge(prometheus.GaugeOpts{
+ Name: "tx_time",
+ Help: "Transition time",
+ Subsystem: machID,
+ Namespace: "mach",
+ }),
+ StatesActiveAmount: prometheus.NewGauge(prometheus.GaugeOpts{
+ Name: "states_active_amount",
+ Help: "Active states amount",
+ Subsystem: machID,
+ Namespace: "mach",
+ }),
+ StatesInactiveAmount: prometheus.NewGauge(prometheus.GaugeOpts{
+ Name: "states_inactive_amount",
+ Help: "Inactive states amount",
+ Subsystem: machID,
+ Namespace: "mach",
+ }),
+ }
+}
+
+// Refresh updates averages values from the interval and updates the gauges.
+func (m *Metrics) Refresh() {
+ if m.closed || m.lastUpdate.Add(m.interval).After(time.Now()) {
+ return
+ }
+
+ m.mx.Lock()
+ defer m.mx.Unlock()
+
+ // update the gauges
+ m.QueueSize.Set(average(m.queueSize, m.queueSizeLen))
+ m.TxTick.Set(average(m.txTick, m.txTickLen))
+ m.StatesActiveAmount.Set(
+ average(m.statesActiveAmount, m.statesActiveAmountLen))
+ m.StatesInactiveAmount.Set(
+ average(m.statesInactiveAmount, m.statesInactiveAmountLen))
+ m.StatesAdded.Set(average(m.statesAdded, m.statesAddedLen))
+ m.StatesRemoved.Set(average(m.statesRemoved, m.statesRemovedLen))
+ m.StatesTouched.Set(average(m.statesTouched, m.statesTouchedLen))
+ m.ExceptionsCount.Set(average(m.exceptionsCount, m.exceptionsCountLen))
+ m.StepsAmount.Set(average(m.stepsAmount, m.stepsAmountLen))
+ m.HandlersAmount.Set(average(m.handlersAmount, m.handlersAmountLen))
+ m.TxTime.Set(average(m.txTime, m.txTimeLen))
+
+ // reset buffers
+ m.queueSize = 0
+ m.queueSizeLen = 0
+ m.txTick = 0
+ m.txTickLen = 0
+ m.statesActiveAmount = 0
+ m.statesActiveAmountLen = 0
+ m.statesInactiveAmount = 0
+ m.statesInactiveAmountLen = 0
+ m.statesAdded = 0
+ m.statesAddedLen = 0
+ m.statesRemoved = 0
+ m.statesRemovedLen = 0
+ m.statesTouched = 0
+ m.statesTouchedLen = 0
+ m.exceptionsCount = 0
+ m.exceptionsCountLen = 0
+ m.stepsAmount = 0
+ m.stepsAmountLen = 0
+ m.handlersAmount = 0
+ m.handlersAmountLen = 0
+ m.txTime = 0
+ m.txTimeLen = 0
+
+ // tag it
+ m.lastUpdate = time.Now()
+}
+
+// Close sets all gauges to 0.
+func (m *Metrics) Close() {
+
+ // close only once
+ if m.closed {
+ return
+ }
+ m.closed = true
+
+ // set all gauges to 0
+ m.StatesAmount.Set(0)
+ m.RelAmount.Set(0)
+ m.RefStatesAmount.Set(0)
+ m.QueueSize.Set(0)
+ m.TxTick.Set(0)
+ m.StatesActiveAmount.Set(0)
+ m.StatesInactiveAmount.Set(0)
+ m.StatesAdded.Set(0)
+ m.StatesRemoved.Set(0)
+ m.StatesTouched.Set(0)
+ m.ExceptionsCount.Set(0)
+ m.StepsAmount.Set(0)
+ m.HandlersAmount.Set(0)
+ m.TxTime.Set(0)
+}
+
+func average(sum uint64, sampleLen uint) float64 {
+ if sampleLen == 0 {
+ return 0
+ }
+
+ return float64(sum / uint64(sampleLen))
+}
+
+// TransitionsToPrometheus bind transitions to Prometheus metrics.
+// TODO debounce
+// TODO bind via the tracer API (so it can be disabled/enabled)
+func TransitionsToPrometheus(
+ mach *machine.Machine, interval time.Duration,
+) *Metrics {
+ metrics := newMetrics(mach, interval)
+
+ // state & relations
+ // TODO bind to EventStatesChange
+ metrics.StatesAmount.Set(float64(len(mach.StateNames)))
+ relCount := 0
+ stateRefCount := 0
+ for _, state := range mach.GetStruct() {
+
+ if state.Add != nil {
+ relCount++
+ stateRefCount += len(state.Add)
+ }
+ if state.Remove != nil {
+ relCount++
+ stateRefCount += len(state.Remove)
+ }
+ if state.Require != nil {
+ relCount++
+ stateRefCount += len(state.Require)
+ }
+ if state.After != nil {
+ relCount++
+ stateRefCount += len(state.After)
+ }
+ }
+ metrics.RelAmount.Set(float64(relCount))
+ metrics.RefStatesAmount.Set(float64(stateRefCount))
+ statesIndex := mach.StateNames[:]
+
+ // TODO extract
+ go func() {
+
+ // bind to transitions
+ var txStartTime time.Time
+ txInitCh := mach.OnEvent(machine.S{machine.EventTransitionInit}, nil)
+ txEndCh := mach.OnEvent(machine.S{machine.EventTransitionEnd}, nil)
+ errStateCh := mach.OnEvent(machine.S{machine.EventException}, nil)
+ prevTime := mach.TimeSum(nil)
+
+ // consume the data from the channels
+ for mach.Ctx.Err() == nil {
+ select {
+
+ case <-mach.Ctx.Done():
+ break
+
+ case <-errStateCh:
+ metrics.ExceptionsCount.Inc()
+
+ case <-txInitCh:
+ txStartTime = time.Now()
+
+ case event := <-txEndCh:
+ if metrics.closed {
+ return
+ }
+
+ tx := event.Args["transition"].(*machine.Transition)
+
+ // skip canceled txs
+ if !tx.Accepted {
+ continue
+ }
+
+ // try to refresh, then lock
+ metrics.Refresh()
+ metrics.mx.Lock()
+
+ queueLen := event.Args["queue_len"].(int)
+ metrics.queueSize = uint64(queueLen)
+ metrics.queueSizeLen++
+ metrics.stepsAmount += uint64(len(tx.Steps))
+ metrics.stepsAmountLen++
+ // TODO log slow txs (Opts and default to 1ms)
+ metrics.txTime += uint64(time.Since(txStartTime).Milliseconds())
+ metrics.txTimeLen++
+
+ // executed handlers
+ handlersCount := 0
+ for _, step := range tx.Steps {
+ if step.Type == machine.StepHandler {
+ handlersCount++
+ }
+ }
+ metrics.handlersAmount += uint64(handlersCount)
+ metrics.handlersAmountLen++
+
+ // tx states
+ added, removed, touched := getTxStates(tx, statesIndex)
+ metrics.statesAdded += uint64(len(added))
+ metrics.statesAddedLen++
+ metrics.statesRemoved += uint64(len(removed))
+ metrics.statesRemovedLen++
+ metrics.statesTouched += uint64(len(touched))
+
+ // time sum
+ currTime := mach.TimeSum(nil)
+ metrics.txTick += currTime - prevTime
+ metrics.txTickLen++
+ prevTime = currTime
+
+ // active / inactive states
+ active := 0
+ inactive := 0
+ for _, t := range tx.ClocksBefore {
+ if machine.IsActiveTick(t) {
+ active++
+ } else {
+ inactive++
+ }
+ }
+ metrics.statesActiveAmount += uint64(active)
+ metrics.statesActiveAmountLen++
+ metrics.statesInactiveAmount += uint64(inactive)
+
+ // unlock
+ metrics.mx.Unlock()
+ }
+ }
+
+ defer metrics.Close()
+ }()
+
+ return metrics
+}
+
+// TODO move to helpers
+func getTxStates(
+ tx *machine.Transition, index machine.S,
+) (added machine.S, removed machine.S, touched machine.S) {
+
+ before := tx.ClocksBefore
+ after := tx.ClocksAfter
+
+ is := func(clocks machine.Clocks, i string) bool {
+ // TODO use T
+ return machine.IsActiveTick(clocks[i])
+ }
+
+ for _, name := range index {
+ if is(before, name) && !is(after, name) {
+ removed = append(removed, name)
+ } else if !is(before, name) && is(after, name) {
+ added = append(added, name)
+ } else if before[name] != after[name] {
+ // treat multi states as added
+ added = append(added, name)
+ }
+ }
+
+ // touched
+ touched = machine.S{}
+ for _, step := range tx.Steps {
+ if step.FromState != "" {
+ touched = append(touched, step.FromState)
+ }
+ if step.ToState != "" {
+ touched = append(touched, step.ToState)
+ }
+ }
+
+ return added, removed, touched
+}
diff --git a/pkg/telemetry/telemetry.go b/pkg/telemetry/telemetry.go
index ff8720c..c394ae4 100644
--- a/pkg/telemetry/telemetry.go
+++ b/pkg/telemetry/telemetry.go
@@ -1,59 +1,76 @@
+// Package telemetry provides telemetry exporters for asyncmachine: am-dbg,
+// Prometheus, and OpenTelemetry.
package telemetry
import (
+ "context"
"encoding/gob"
"fmt"
"log"
"net/rpc"
+ "slices"
+ "strings"
+ "sync"
+ "time"
"github.com/samber/lo"
+ "go.opentelemetry.io/otel/attribute"
+ "go.opentelemetry.io/otel/trace"
am "github.com/pancsta/asyncmachine-go/pkg/machine"
)
-const RpcHost = "localhost:9823"
+//////////////////
+///// AM-DBG
+//////////////////
-// Msg is the interface for the messages to be sent to the am-dbg server.
-type Msg interface {
+const DbgHost = "localhost:6831"
+
+// DbgMsg is the interface for the messages to be sent to the am-dbg server.
+type DbgMsg interface {
// Clock returns the state's clock, using the passed index
Clock(statesIndex am.S, state string) uint64
- // Is true if the state is active, using the passed index
+ // Is returns true if the state is active, using the passed index
Is(statesIndex am.S, states am.S) bool
}
-// MsgStruct contains the state and relations data.
-type MsgStruct struct {
+// DbgMsgStruct contains the state and relations data.
+type DbgMsgStruct struct {
// Machine ID
ID string
// state names defining the indexes for diffs
StatesIndex am.S
// all the states with relations
- States am.States
+ States am.Struct
// log level of the machine
LogLevel am.LogLevel
}
-func (d *MsgStruct) Clock(_ am.S, _ string) uint64 {
+func (d *DbgMsgStruct) Clock(_ am.S, _ string) uint64 {
return 0
}
-func (d *MsgStruct) Is(_ am.S, _ am.S) bool {
+func (d *DbgMsgStruct) Is(_ am.S, _ am.S) bool {
return false
}
-// MsgTx contains transition data.
-type MsgTx struct {
+// DbgMsgTx contains transition data.
+type DbgMsgTx struct {
+ MachineID string
// Transition ID
ID string
- // StatesIndex-based active indicators
- StatesActive []bool
// map of positions from the index to state clocks
- Clocks []uint64
+ Clocks am.T
// result of the transition
Accepted bool
+ // mutation type
+ Type am.MutationType
+ // called states
+ CalledStates []string
// all the transition steps
- Steps []*am.TransitionStep
+ Steps []*am.Step
// log entries created during the transition
+ // TODO include log levels
LogEntries []string
// log entries before the transition, which happened after the prev one
PreLogEntries []string
@@ -61,107 +78,629 @@ type MsgTx struct {
IsAuto bool
// queue length at the start of the transition
Queue int
+ // dont send this over the wire
+ Time *time.Time
}
-func (d *MsgTx) Clock(statesIndex am.S, state string) uint64 {
+func (d *DbgMsgTx) Clock(statesIndex am.S, state string) uint64 {
idx := lo.IndexOf(statesIndex, state)
return d.Clocks[idx]
}
-func (d *MsgTx) Is(statesIndex am.S, states am.S) bool {
+func (d *DbgMsgTx) Is(statesIndex am.S, states am.S) bool {
for _, state := range states {
idx := lo.IndexOf(statesIndex, state) //nolint:typecheck
- if !d.StatesActive[idx] {
+ if idx == -1 {
+ // TODO handle err (log?)
+ panic("unknown state: " + state)
+ }
+
+ if !am.IsActiveTick(d.Clocks[idx]) {
return false
}
}
+
return true
}
-type rpcClient struct {
+func (d *DbgMsgTx) Is1(statesIndex am.S, state string) bool {
+ return d.Is(statesIndex, am.S{state})
+}
+
+func (d *DbgMsgTx) TimeSum() uint64 {
+ sum := uint64(0)
+ for _, clock := range d.Clocks {
+ sum += clock
+ }
+
+ return sum
+}
+
+type dbgClient struct {
client *rpc.Client
}
-func newClient(url string) (*rpcClient, error) {
- log.Printf("Connecting to %s", url)
+func newDbgClient(url string) (*dbgClient, error) {
+ // log.Printf("Connecting to %s", url)
client, err := rpc.Dial("tcp", url)
if err != nil {
return nil, err
}
- return &rpcClient{client: client}, nil
+
+ return &dbgClient{client: client}, nil
}
-func (c *rpcClient) sendMsgTx(msg *MsgTx) error {
+func (c *dbgClient) sendMsgTx(msg *DbgMsgTx) error {
var reply string
// TODO use Go() to not block
- err := c.client.Call("RPCServer.MsgTx", msg, &reply)
+ err := c.client.Call("RPCServer.DbgMsgTx", msg, &reply)
if err != nil {
return err
}
return nil
}
-func (c *rpcClient) sendMsgStruct(msg *MsgStruct) error {
+func (c *dbgClient) sendMsgStruct(msg *DbgMsgStruct) error {
var reply string
// TODO use Go() to not block
- err := c.client.Call("RPCServer.MsgStruct", msg, &reply)
+ err := c.client.Call("RPCServer.DbgMsgStruct", msg, &reply)
if err != nil {
return err
}
+
return nil
}
-func MonitorTransitions(m *am.Machine, url string) error {
- var err error
+// TransitionsToDBG sends transitions to the am-dbg server.
+// TODO test TransitionsToDBG
+// TODO support changes to mach.StateNames
+func TransitionsToDBG(mach *am.Machine, url string) error {
gob.Register(am.Relation(0))
- client, err := newClient(url)
+
+ if url == "" {
+ url = DbgHost
+ }
+ client, err := newDbgClient(url)
if err != nil {
return fmt.Errorf("failed to connect to am-dbg: %w", err)
}
- msg := &MsgStruct{
- ID: m.ID,
- StatesIndex: m.StateNames,
- States: m.States,
- LogLevel: m.GetLogLevel(),
- }
- // TODO retries
- err = client.sendMsgStruct(msg)
- if err != nil {
- return fmt.Errorf("failed to send a msg to am-dbg: %w", err)
+
+ err2 := sendStructMsg(mach, client)
+ if err2 != nil {
+ return err2
}
go func() {
- // bind to transitions
- txEndCh := m.On([]string{am.EventTransitionEnd}, nil)
+ // bind to transitions steam and state changes
+ txEndCh := mach.OnEvent(am.S{am.EventTransitionEnd}, nil)
+ newStructCh := mach.OnEvent(am.S{am.EventStructChange}, nil)
+
// send incoming transitions
- for event := range txEndCh {
- tx := event.Args["transition"].(*am.Transition)
- preLogs := event.Args["pre_logs"].([]string)
- queueLen := event.Args["queue_len"].(int)
- msg := &MsgTx{
- ID: tx.ID,
- StatesActive: make([]bool, len(m.StateNames)),
- Clocks: make([]uint64, len(m.StateNames)),
- Accepted: tx.Accepted,
- Steps: lo.Map(tx.Steps,
- func(step *am.TransitionStep, _ int) *am.TransitionStep {
- return step
- }),
- LogEntries: tx.LogEntries,
- PreLogEntries: preLogs,
- IsAuto: tx.IsAuto(),
- Queue: queueLen,
- }
- for i, state := range m.StateNames {
- msg.StatesActive[i] = m.Is(am.S{state})
- msg.Clocks[i] = m.Clock(state)
- }
- // TODO retries
- err := client.sendMsgTx(msg)
- if err != nil {
- log.Println("failed to send a msg to am-dbg: " + url + err.Error())
+ for mach.Ctx.Err() == nil {
+ select {
+
+ case <-mach.Ctx.Done():
+ client.client.Close()
return
+
+ // states/relations have changed
+ case <-newStructCh:
+ // TODO support struct patches as DbgMsgTx.StructPatch
+ err := sendStructMsg(mach, client)
+ if err != nil {
+ log.Println("failed to send a msg to am-dbg: " + url + err.Error())
+ }
+
+ // new transition
+ case event := <-txEndCh:
+
+ // params
+ tx, ok1 := event.Args["transition"].(*am.Transition)
+ preLogs, ok2 := event.Args["pre_logs"].([]string)
+ queueLen, ok3 := event.Args["queue_len"].(int)
+ if !ok1 || !ok2 || !ok3 {
+ log.Println("invalid transition end event")
+ continue
+ }
+
+ msg := &DbgMsgTx{
+ MachineID: mach.ID,
+ ID: tx.ID,
+ Clocks: make([]uint64, len(tx.ClocksAfter)),
+ Accepted: tx.Accepted,
+ Type: tx.Mutation.Type,
+ CalledStates: tx.CalledStates(),
+
+ Steps: lo.Map(tx.Steps,
+ func(step *am.Step, _ int) *am.Step {
+ return step
+ }),
+ LogEntries: tx.LogEntries,
+ PreLogEntries: preLogs,
+ IsAuto: tx.IsAuto(),
+ Queue: queueLen,
+ }
+
+ if mach.LogID {
+ removeLogPrefix(msg)
+ }
+
+ for i, state := range mach.StateNames {
+ msg.Clocks[i] = tx.ClocksAfter[state]
+ }
+
+ // TODO retries
+ err := client.sendMsgTx(msg)
+ if err != nil {
+ log.Println("failed to send a msg to am-dbg: " + url + err.Error())
+ return
+ }
}
}
}()
+
+ return nil
+}
+
+// sendStructMsg sends the machine's states and relations
+func sendStructMsg(mach *am.Machine, client *dbgClient) error {
+ msg := &DbgMsgStruct{
+ ID: mach.ID,
+ StatesIndex: mach.StateNames,
+ States: mach.GetStruct(),
+ LogLevel: mach.GetLogLevel(),
+ }
+
+ // TODO retries
+ err := client.sendMsgStruct(msg)
+ if err != nil {
+ return fmt.Errorf("failed to send a msg to am-dbg: %w", err)
+ }
+
return nil
}
+
+func removeLogPrefix(msg *DbgMsgTx) {
+ maxIDlen := 5
+ addChars := 3 // "[] "
+ prefixLen := min(len(msg.MachineID)+addChars, maxIDlen+addChars)
+
+ for i := range msg.LogEntries {
+ if len(msg.LogEntries[i]) < prefixLen {
+ continue
+ }
+ msg.LogEntries[i] = msg.LogEntries[i][prefixLen:]
+ }
+
+ for i := range msg.PreLogEntries {
+ if len(msg.PreLogEntries[i]) < prefixLen {
+ continue
+ }
+ msg.PreLogEntries[i] = msg.PreLogEntries[i][prefixLen:]
+ }
+}
+
+//////////////////
+///// OPEN TELEMETRY
+//////////////////
+
+// OtelMachTracer implements machine.Tracer for OpenTelemetry.
+// Support tracing multiple machines
+type OtelMachTracer struct {
+ Tracer trace.Tracer
+ Machines map[string]*OtelMachineData
+ MachinesMx sync.Mutex
+ MachinesOrder []string
+ Logf func(format string, args ...any)
+ // map of parent Span for each submachine
+ parentSpans map[string]trace.Span
+ // child-parent map, used for parentSpans
+ parents map[string]string
+
+ opts *OtelMachTracerOpts
+ ended bool
+}
+
+// TODO runtime typecheck for am.Tracer
+
+type OtelMachineData struct {
+ ID string
+ // handler ctx & span to be used for more detailed tracing inside handlers
+ HandlerTrace context.Context
+ HandlerSpan trace.Span
+ Lock sync.Mutex
+ Ended bool
+
+ machTrace context.Context
+ txTrace context.Context
+ // per-state group traces
+ stateNames map[string]context.Context
+ // per-state traces
+ stateInstances map[string]context.Context
+ // group trace for all state name groups
+ stateGroup context.Context
+ // group trace for all the transitions
+ txGroup context.Context
+}
+
+type OtelMachTracerOpts struct {
+ // if true, only state changes will be traced
+ SkipTransitions bool
+ Logf func(format string, args ...any)
+}
+
+// NewOtelMachTracer creates a new machine tracer from an OpenTelemetry tracer.
+// Requires OtelMachTracer.Dispose to be called at the end.
+func NewOtelMachTracer(tracer trace.Tracer, opts *OtelMachTracerOpts,
+) *OtelMachTracer {
+ if tracer == nil {
+ panic("nil tracer")
+ }
+ if opts == nil {
+ opts = &OtelMachTracerOpts{}
+ }
+ otel := &OtelMachTracer{
+ Tracer: tracer,
+ Machines: make(map[string]*OtelMachineData),
+ opts: opts,
+ parentSpans: make(map[string]trace.Span),
+ parents: make(map[string]string),
+ }
+ if opts.Logf != nil {
+ otel.Logf = opts.Logf
+ } else {
+ otel.Logf = func(format string, args ...any) {}
+ }
+ otel.Logf("[otel] NewOtelMachTracer")
+ return otel
+}
+
+func (ot *OtelMachTracer) getMachineData(id string) *OtelMachineData {
+ ot.MachinesMx.Lock()
+ defer ot.MachinesMx.Unlock()
+
+ if data, ok := ot.Machines[id]; ok {
+ return data
+ }
+ data := &OtelMachineData{
+ stateInstances: make(map[string]context.Context),
+ stateNames: make(map[string]context.Context),
+ }
+ if !ot.ended {
+ ot.Logf("[otel] getMachineData: creating for %s", id)
+ ot.Machines[id] = data
+ ot.MachinesOrder = append(ot.MachinesOrder, id)
+ }
+ return data
+}
+
+func (ot *OtelMachTracer) MachineInit(mach *am.Machine) {
+ name := "mach:" + mach.ID
+
+ // nest under parent
+ ctx := mach.Ctx
+ pid, ok := ot.parents[mach.ID]
+ if ok {
+ if parentSpan, ok := ot.parentSpans[pid]; ok {
+ ctx = trace.ContextWithSpan(ctx, parentSpan)
+ }
+ }
+
+ // create a machine trace
+ machCtx, _ := ot.Tracer.Start(ctx, name, trace.WithAttributes(
+ attribute.String("id", mach.ID),
+ ))
+
+ // add tracing to machine's context
+ ot.Logf("[otel] MachineInit: trace %s", mach.ID)
+ mach.Ctx = machCtx
+ data := ot.getMachineData(mach.ID)
+ data.machTrace = machCtx
+
+ data.ID = mach.ID
+
+ // create a group span for states
+ stateGroupCtx, stateGroupSpan := ot.Tracer.Start(machCtx, "states",
+ trace.WithAttributes(attribute.String("mach_id", mach.ID)))
+ data.stateGroup = stateGroupCtx
+ // groups are only for nesting, so end it right away
+ stateGroupSpan.End()
+
+ if !ot.opts.SkipTransitions {
+ // create a group span for transitions
+ txGroupCtx, txGroupSpan := ot.Tracer.Start(machCtx, "transitions",
+ trace.WithAttributes(attribute.String("mach_id", mach.ID)))
+ data.txGroup = txGroupCtx
+ // groups are only for nesting, so end it right away
+ txGroupSpan.End()
+ }
+}
+
+// NewSubmachine links 2 machines with a parent-child relationship.
+func (ot *OtelMachTracer) NewSubmachine(parent, mach *am.Machine) {
+ if ot.ended {
+ ot.Logf("[otel] NewSubmachine: tracer already ended, ignoring %s",
+ mach.ID)
+ return
+ }
+ _, ok := ot.parents[mach.ID]
+ if ok {
+ panic("Submachine already being traced (duplicate ID " + mach.ID + ")")
+ }
+ ot.parents[mach.ID] = parent.ID
+ data := ot.getMachineData(parent.ID)
+ data.Lock.Lock()
+ defer data.Lock.Unlock()
+
+ if data.Ended {
+ ot.Logf("[otel] NewSubmachine: parent %s already ended", parent.ID)
+ return
+ }
+
+ if _, ok := ot.parentSpans[parent.ID]; !ok {
+ // create a group span for submachines
+ _, submachGroupSpan := ot.Tracer.Start(
+ data.machTrace, "submachines",
+ trace.WithAttributes(attribute.String("mach_id", parent.ID)))
+ ot.parentSpans[parent.ID] = submachGroupSpan
+ // groups are only for nesting, so end it right away
+ submachGroupSpan.End()
+ }
+}
+
+func (ot *OtelMachTracer) MachineDispose(id string) {
+ ot.MachinesMx.Lock()
+ defer ot.MachinesMx.Unlock()
+
+ ot.doDispose(id)
+}
+
+func (ot *OtelMachTracer) doDispose(id string) {
+ data, ok := ot.Machines[id]
+ if !ok {
+ ot.Logf("[otel] MachineDispose: machine %s not found", id)
+ return
+ }
+ ot.Logf("[otel] MachineDispose: disposing %s", id)
+ data.Lock.Lock()
+
+ delete(ot.parentSpans, id)
+ delete(ot.Machines, id)
+ ot.MachinesOrder = lo.Without(ot.MachinesOrder, id)
+ data.Ended = true
+
+ // transitions
+ if data.HandlerSpan != nil {
+ data.HandlerSpan.End()
+ }
+ if data.txTrace != nil {
+ trace.SpanFromContext(data.txTrace).End()
+ }
+
+ // states
+ for _, ctx := range data.stateInstances {
+ trace.SpanFromContext(ctx).End()
+ }
+ for _, ctx := range data.stateNames {
+ trace.SpanFromContext(ctx).End()
+ }
+
+ // groups
+ trace.SpanFromContext(data.stateGroup).End()
+ trace.SpanFromContext(data.txGroup).End()
+ trace.SpanFromContext(data.machTrace).End()
+ data.Lock.Unlock()
+}
+
+func (ot *OtelMachTracer) TransitionInit(tx *am.Transition) {
+ if ot.ended {
+ ot.Logf("[otel] TransitionInit: tracer already ended, ignoring %s",
+ tx.Machine.ID)
+ return
+ }
+ data := ot.getMachineData(tx.Machine.ID)
+ if data.Ended {
+ ot.Logf("[otel] TransitionInit: machine %s already ended", tx.Machine.ID)
+ return
+ }
+ // if skipping transitions, only create the machine data for states
+ if ot.opts.SkipTransitions {
+ return
+ }
+ mutLabel := fmt.Sprintf("[%s] %s",
+ tx.Mutation.Type, j(tx.Mutation.CalledStates))
+ name := mutLabel
+
+ // support exceptions along with the passed error
+ var errAttr error
+ if lo.Contains(tx.TargetStates, am.Exception) {
+ name = "exception"
+ if tx.Args() != nil {
+ if err, ok := tx.Args()["err"].(error); ok {
+ errAttr = err
+ }
+ }
+ }
+ // build a regular trace
+ ctx, span := ot.Tracer.Start(data.txGroup, name, trace.WithAttributes(
+ attribute.String("tx_id", tx.ID),
+ attribute.Int64("time_before", timeSum(tx.ClocksBefore)),
+ attribute.String("mutation", mutLabel),
+ ))
+ // decorate Exception trace
+ if errAttr != nil {
+ span.SetAttributes(
+ attribute.String("error", errAttr.Error()),
+ )
+ }
+ // trace logged args, if any
+ argsMatcher := tx.Machine.GetLogArgs()
+ if argsMatcher != nil {
+ for param, val := range argsMatcher(tx.Args()) {
+ span.SetAttributes(
+ attribute.String("args."+param, val),
+ )
+ }
+ }
+ // expose
+ data.txTrace = ctx
+}
+
+func (ot *OtelMachTracer) TransitionEnd(tx *am.Transition) {
+ if ot.ended {
+ ot.Logf("[otel] TransitionEnd: tracer already ended, ignoring %s",
+ tx.Machine.ID)
+ return
+ }
+ data := ot.getMachineData(tx.Machine.ID)
+ if data.Ended {
+ ot.Logf("[otel] TransitionEnd: machine %s already ended", tx.Machine.ID)
+ return
+ }
+ data.Lock.Lock()
+ defer data.Lock.Unlock()
+
+ statesAdded := am.DiffStates(tx.TargetStates, tx.StatesBefore)
+ statesRemoved := am.DiffStates(tx.StatesBefore, tx.TargetStates)
+ // support multi states
+ for name, tick := range tx.ClocksAfter {
+ if tick > 1+tx.ClocksBefore[name] && !lo.Contains(statesAdded, name) {
+ statesAdded = append(statesAdded, name)
+ }
+ }
+
+ // handle transition
+ statesDiff := ""
+ if len(statesAdded) > 0 {
+ statesDiff += "+" + jw(statesAdded, " +")
+ }
+ if len(statesRemoved) > 0 {
+ statesDiff += " -" + jw(statesRemoved, " -")
+ }
+ if !ot.opts.SkipTransitions && data.txTrace != nil {
+ span := trace.SpanFromContext(data.txTrace)
+ span.SetAttributes(
+ attribute.String("states_diff", strings.Trim(statesDiff, " ")),
+ attribute.Int64("time_after", timeSum(tx.ClocksAfter)),
+ attribute.Bool("accepted", tx.Accepted),
+ attribute.Int("steps_count", len(tx.Steps)),
+ )
+ span.End()
+ data.txTrace = nil
+ }
+
+ // handle state changes
+ // remove old states
+ for _, state := range statesRemoved {
+ if ctx, ok := data.stateInstances[state]; ok {
+ trace.SpanFromContext(ctx).End()
+ delete(data.stateInstances, state)
+ }
+ }
+
+ // add new state trace, with a group if needed
+ for _, state := range statesAdded {
+ if data.Ended {
+ ot.Logf("[otel] TransitionEnd: machine %s already ended", tx.Machine.ID)
+ break
+ }
+ // name group
+ nameCtx, ok := data.stateNames[state]
+ if !ok {
+ // create a new state name group trace, but end it right away
+ ctx, span := ot.Tracer.Start(data.stateGroup, state)
+ nameCtx = ctx
+ data.stateNames[state] = nameCtx
+ span.End()
+ }
+ // multi state - end the prev instance
+ _, ok = data.stateInstances[state]
+ if ok {
+ trace.SpanFromContext(data.stateInstances[state]).End()
+ }
+ // TODO use trace.WithLinks() to the tx span, which contains args
+ ctx, _ := ot.Tracer.Start(nameCtx, state, trace.WithAttributes(
+ attribute.String("tx_id", tx.ID),
+ ))
+ data.stateInstances[state] = ctx
+ }
+}
+
+func (ot *OtelMachTracer) HandlerStart(
+ tx *am.Transition, emitter string, handler string,
+) {
+ if ot.ended {
+ return
+ }
+ if ot.opts.SkipTransitions {
+ return
+ }
+ data := ot.getMachineData(tx.Machine.ID)
+ if data.Ended {
+ return
+ }
+ name := handler
+ ctx, span := ot.Tracer.Start(data.txTrace, name, trace.WithAttributes(
+ attribute.String("name", handler),
+ attribute.String("emitter", emitter),
+ ))
+ data.HandlerTrace = ctx
+ data.HandlerSpan = span
+}
+
+func (ot *OtelMachTracer) HandlerEnd(tx *am.Transition, _ string, _ string) {
+ if ot.ended {
+ return
+ }
+ if ot.opts.SkipTransitions {
+ return
+ }
+ data := ot.getMachineData(tx.Machine.ID)
+ if data.Ended {
+ return
+ }
+ trace.SpanFromContext(data.HandlerTrace).End()
+ data.HandlerTrace = nil
+ data.HandlerSpan = nil
+}
+
+func (ot *OtelMachTracer) End() {
+ ot.MachinesMx.Lock()
+ defer ot.MachinesMx.Unlock()
+
+ ot.Logf("[otel] End")
+ ot.ended = true
+ // end traces in reverse order
+ slices.Reverse(ot.MachinesOrder)
+
+ for _, id := range ot.MachinesOrder {
+ ot.doDispose(id)
+ }
+
+ ot.Machines = nil
+}
+
+func (ot *OtelMachTracer) Inheritable() bool {
+ return true
+}
+
+//////////////////
+///// UTILS
+//////////////////
+
+// j joins state names
+func j(states []string) string {
+ return strings.Join(states, " ")
+}
+
+// jw joins state names with `sep`.
+func jw(states []string, sep string) string {
+ return strings.Join(states, sep)
+}
+
+func timeSum(clocks am.Clocks) int64 {
+ sum := int64(0)
+ for _, clock := range clocks {
+ sum += int64(clock)
+ }
+ return sum
+}
diff --git a/pkg/x/helpers/helpers.go b/pkg/x/helpers/helpers.go
new file mode 100644
index 0000000..a32ad26
--- /dev/null
+++ b/pkg/x/helpers/helpers.go
@@ -0,0 +1,120 @@
+// Package helpers provides some utility functions for asyncmachine, which are
+// out of scope of the main package.
+package helpers
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "slices"
+
+ am "github.com/pancsta/asyncmachine-go/pkg/machine"
+)
+
+func RaceCtx[T *any](ctx context.Context, ch chan T) (T, error) {
+ select {
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ case v := <-ch:
+ return v, nil
+ }
+}
+
+// NestedState forwards the mutation to one of the composed submachines. Parent
+// state should be a Multi state and only called directly (not from a relation).
+// TODO test case, solve locking by passing the event to the submachine
+func NestedState(
+ e *am.Event, strIDField string, machGetter func(id string) *am.Machine,
+) (am.Result, chan struct{}, error) {
+ // validate
+ if e.Mutation().Type != am.MutationAdd {
+ return am.Canceled, nil, fmt.Errorf(
+ "unsupported nested mutation %s", e.Mutation().Type)
+ }
+
+ // extract ID from params
+ strID, ok := e.Args[strIDField].(string)
+ if !ok || strID == "" {
+ return am.Canceled, nil, am.ErrInvalidArgs
+ }
+
+ // init vars
+ submach := machGetter(strID)
+ if submach == nil {
+ return am.Canceled, nil, fmt.Errorf("submachine %s not found", strID)
+ }
+ state := e.Name[0 : len(e.Name)-5]
+
+ // ignore active non-multi nested states
+ tick := submach.Clock(state)
+ isMulti := submach.GetStruct()[state].Multi
+ if am.IsActiveTick(tick) && !isMulti {
+ return am.Canceled, nil, fmt.Errorf("nested state %s is active", state)
+ }
+
+ // fwd the state
+ res := submach.Add1(state, e.Args)
+
+ // handle queuing with a timeout
+ if res == am.Queued && isMulti {
+ // wait for the state to be activated again
+ when := submach.WhenTime(am.S{state}, am.T{tick + 2}, nil)
+ return res, when, nil
+ } else if res == am.Queued {
+ when := submach.When1(state, nil)
+ return res, when, nil
+ }
+
+ return res, nil, nil
+}
+
+// ErrFromCtxs returns the first non-nil error from a list of contexts.
+func ErrFromCtxs(ctxs ...context.Context) error {
+ for _, ctx := range ctxs {
+ if ctx.Err() != nil {
+ return ctx.Err()
+ }
+ }
+
+ return nil
+}
+
+// StatesToIndexes converts a list of state names to a list of state indexes,
+// for a given machine.
+func StatesToIndexes(mach *am.Machine, states am.S) []int {
+ var indexes []int
+ for _, state := range states {
+ indexes = append(indexes, slices.Index(mach.StateNames, state))
+ }
+
+ return indexes
+}
+
+// IndexesToStates converts a list of state indexes to a list of state names,
+// for a given machine.
+func IndexesToStates(mach *am.Machine, indexes []int) am.S {
+ states := am.S{}
+ for _, index := range indexes {
+ states = append(states, mach.StateNames[index])
+ }
+
+ return states
+}
+
+// TimeMatrix returns a matrix of state clocks for the given machines.
+func TimeMatrix(machines []*am.Machine) ([]am.T, error) {
+ if len(machines) == 0 {
+ return nil, errors.New("no machines provided")
+ }
+
+ matrix := make([]am.T, len(machines))
+ prevLen := len(machines[0].GetStruct())
+ for i, mach := range machines {
+ if len(mach.GetStruct()) != prevLen {
+ return nil, errors.New("machines have different state lengths")
+ }
+ matrix[i] = mach.Time(nil)
+ }
+
+ return matrix, nil
+}
diff --git a/pkg/x/helpers/helpers_test.go b/pkg/x/helpers/helpers_test.go
new file mode 100644
index 0000000..d26b445
--- /dev/null
+++ b/pkg/x/helpers/helpers_test.go
@@ -0,0 +1,81 @@
+package helpers
+
+import (
+ "context"
+ "os"
+ "testing"
+ "time"
+
+ am "github.com/pancsta/asyncmachine-go/pkg/machine"
+ "github.com/stretchr/testify/assert"
+)
+
+type S = am.S
+type T = am.T
+
+func TestTimeMatrix(t *testing.T) {
+ // m1 init
+ m1 := NewNoRels(t, nil)
+ statesStruct := m1.GetStruct()
+ statesStruct["B"] = am.State{Multi: true}
+ err := m1.SetStruct(statesStruct, S{"A", "B", "C", "D", am.Exception})
+ assert.NoError(t, err)
+
+ // mutate & assert
+ m1.Add(S{"A", "B"}, nil)
+ m1.Add(S{"A", "B"}, nil)
+ assertClocks(t, m1, S{"A", "B", "C", "D"}, T{1, 3, 0, 0},
+ "m1 clocks mismatch")
+
+ // m2 init
+ m2 := NewNoRels(t, nil)
+ statesStruct = m1.GetStruct()
+ statesStruct["B"] = am.State{Multi: true}
+ err = m2.SetStruct(statesStruct, S{"A", "B", "C", "D", am.Exception})
+ assert.NoError(t, err)
+
+ // mutate & assert
+ m2.Add(S{"A", "B"}, nil)
+ m2.Add(S{"A", "B"}, nil)
+ m2.Add(S{"A", "B", "C"}, nil)
+ m2.Set(S{"D"}, nil)
+ assertClocks(t, m2, S{"A", "B", "C", "D"}, T{2, 6, 2, 1},
+ "m2 clocks mismatch")
+
+ matrix, err := TimeMatrix([]*am.Machine{m1, m2})
+ assert.NoError(t, err)
+ assert.Equal(t, T{1, 3, 0, 0, 0}, matrix[0])
+ assert.Equal(t, T{2, 6, 2, 1, 0}, matrix[1])
+}
+
+///// helpers
+// TODO extract test helpers to internal/testing
+
+// NewNoRels creates a new machine with no relations between states.
+func NewNoRels(t *testing.T, initialState am.S) *am.Machine {
+ m := am.New(context.Background(), am.Struct{
+ "A": {},
+ "B": {},
+ "C": {},
+ "D": {},
+ }, nil)
+ m.SetLogger(func(i am.LogLevel, msg string, args ...any) {
+ t.Logf(msg, args...)
+ })
+ if os.Getenv("AM_DEBUG") != "" {
+ m.SetLogLevel(am.LogEverything)
+ m.HandlerTimeout = 2 * time.Minute
+ }
+ if initialState != nil {
+ m.Set(initialState, nil)
+ }
+ return m
+}
+
+func assertClocks(t *testing.T, m *am.Machine, states S, times T,
+ msgAndArgs ...interface{},
+) {
+ for i, state := range states {
+ assert.True(t, m.IsClock(state, times[i]), msgAndArgs...)
+ }
+}
diff --git a/scripts/am_grafana.py b/scripts/am_grafana.py
new file mode 100644
index 0000000..722f5b7
--- /dev/null
+++ b/scripts/am_grafana.py
@@ -0,0 +1,157 @@
+from grafanalib.core import (
+ TimeSeries, RowPanel, Histogram,
+ Target, GridPos,
+)
+
+w = 12
+h = 8
+interval = '5s'
+
+x = 0
+y = 0
+nextCol = 0
+row = 0
+
+
+def next_grid_pos():
+ global x, y, w, h, nextCol
+
+ pos = GridPos(x=x, y=y, w=w, h=h)
+ if nextCol == 0:
+ nextCol = 1
+ x = w
+ else:
+ nextCol = 0
+ y += h
+ x = 0
+
+ return pos
+
+
+def next_row_pos():
+ global x, y, w, h, nextCol
+
+ if nextCol == 1:
+ x = 0
+ y += h + 1
+ nextCol = 0
+
+ pos = GridPos(x=0, y=y, w=w * 2, h=1)
+ y += 1
+
+ return pos
+
+
+def mach_panels(id):
+ # replace - with _
+ id = id.replace('-', '_')
+
+ return [
+ RowPanel(title="Machine: " + id, gridPos=next_row_pos()),
+
+ TimeSeries(
+ title="Transition changes",
+ gridPos=next_grid_pos(),
+ dataSource='prometheus',
+ interval=interval,
+ targets=[
+
+ Target(
+ legendFormat="Queue size",
+ expr='mach_' + id + '_queue_size',
+ ),
+ Target(
+ legendFormat="Tx ticks",
+ expr='mach_' + id + '_tx_tick',
+ ),
+
+ Target(
+ legendFormat="Number of tx steps",
+ expr='mach_' + id + '_steps_amount',
+ ),
+ Target(
+ legendFormat="Called handlers",
+ expr='mach_' + id + '_handlers_amount',
+ ),
+
+ Target(
+ legendFormat="States added",
+ expr='mach_' + id + '_states_added',
+ ),
+ Target(
+ legendFormat="States removed",
+ expr='mach_' + id + '_states_removed',
+ ),
+
+ Target(
+ legendFormat="States touched",
+ expr='mach_' + id + '_touched',
+ ),
+ ],
+ ),
+
+ TimeSeries(
+ title="Machine states",
+ gridPos=next_grid_pos(),
+ dataSource='prometheus',
+ interval=interval,
+ targets=[
+
+ Target(
+ legendFormat="States referenced in relations",
+ expr='mach_' + id + '_ref_states_amount',
+ ),
+ Target(
+ legendFormat="Relations",
+ expr='mach_' + id + '_relations_amount',
+ ),
+ Target(
+ legendFormat="States active",
+ expr='mach_' + id + '_states_active_amount',
+ ),
+ Target(
+ legendFormat="States inactive",
+ expr='mach_' + id + '_states_inactive_amount',
+ ),
+ ],
+ ),
+
+ TimeSeries(
+ title="Transition time",
+ gridPos=next_grid_pos(),
+ dataSource='prometheus',
+ interval=interval,
+ targets=[
+ Target(
+ legendFormat="Tx time (ms)",
+ expr='mach_' + id + '_tx_time / 1000',
+ ),
+ ],
+ ),
+
+ Histogram(
+ title="Average transition time",
+ gridPos=next_grid_pos(),
+ dataSource='prometheus',
+ interval=interval,
+ targets=[
+ Target(
+ legendFormat="Tx time (ms)",
+ expr='mach_' + id + '_tx_time / 1000',
+ ),
+ ],
+ ),
+
+ TimeSeries(
+ title="Exceptions",
+ gridPos=next_grid_pos(),
+ dataSource='prometheus',
+ interval=interval,
+ targets=[
+ Target(
+ legendFormat="Exceptions",
+ expr='mach_' + id + '_exceptions_count',
+ ),
+ ],
+ ),
+ ]
diff --git a/scripts/dep-taskfile.sh b/scripts/dep-taskfile.sh
new file mode 100755
index 0000000..9b41858
--- /dev/null
+++ b/scripts/dep-taskfile.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+# check if installed
+if command -v task >/dev/null; then echo "OK" && exit; fi
+
+echo "Visit https://taskfile.dev/installation/ for more info"
+echo "----- ----- -----"
+
+sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b ~/.local/bin
diff --git a/scripts/mach.dashboard.py b/scripts/mach.dashboard.py
new file mode 100644
index 0000000..22aac59
--- /dev/null
+++ b/scripts/mach.dashboard.py
@@ -0,0 +1,35 @@
+import os
+import importlib.util
+from grafanalib.core import Dashboard
+
+def import_from_path(path):
+ spec = importlib.util.spec_from_file_location("module.name", path)
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ return module
+
+# include the shared am grafana funcs
+am_grafana = import_from_path(os.getcwd()+"/am_grafana.py")
+
+interval = '5s'
+panels = []
+
+# loop over env var IDS, divided by comma
+for id in os.environ['IDS'].split(','):
+ # replace - with _
+ id = id.replace('-', '_')
+
+ # append to existing panels
+ panels.extend(am_grafana.mach_panels(id))
+
+
+dashboard = Dashboard(
+ title="Machine Dashboard: " + os.environ['IDS'],
+ description="AsyncMachine internals gathered by pkg/telemetry",
+ refresh='5s',
+ # tags=[
+ # 'example'
+ # ],
+ timezone="browser",
+ panels=panels,
+).auto_panel_ids()
diff --git a/tools/am-dbg/debugger/debugger.go b/tools/am-dbg/debugger/debugger.go
deleted file mode 100644
index c701462..0000000
--- a/tools/am-dbg/debugger/debugger.go
+++ /dev/null
@@ -1,660 +0,0 @@
-package debugger
-
-import (
- "fmt"
- "strings"
- "time"
-
- "code.rocketnine.space/tslocum/cbind"
- "github.com/gdamore/tcell/v2"
- "github.com/pancsta/cview"
- "github.com/samber/lo"
-
- "github.com/lithammer/dedent"
- am "github.com/pancsta/asyncmachine-go/pkg/machine"
- "github.com/pancsta/asyncmachine-go/pkg/telemetry"
- ss "github.com/pancsta/asyncmachine-go/tools/am-dbg/states"
-)
-
-const (
- // TODO light mode
- colorActive = tcell.ColorOlive
- colorInactive = tcell.ColorLimeGreen
- colorHighlight = tcell.ColorDarkSlateGray
- playInterval = 500 * time.Millisecond
-)
-
-type Debugger struct {
- am.ExceptionHandler
- Mach *am.Machine
- EnableMouse bool
- app *cview.Application
- tree *cview.TreeView
- treeRoot *cview.TreeNode
- log *cview.TextView
- timelineTxs *cview.ProgressBar
- timelineSteps *cview.ProgressBar
- focusable []*cview.Box
- selectedState string
- msgStruct *telemetry.MsgStruct
- msgTxs []*telemetry.MsgTx
- // current transition, 1-based
- cursorTx int
- // current step, 1-based
- cursorStep int
- playTimer *time.Ticker
- currTxBarRight *cview.TextView
- currTxBarLeft *cview.TextView
- nextTxBarLeft *cview.TextView
- nextTxBarRight *cview.TextView
- msgTxsParsed []MsgTxParsed
- helpScreen *cview.Flex
- keystrokesBar *cview.TextView
- layoutRoot *cview.Panels
-}
-
-type MsgTxParsed struct {
- Time time.Time
- StatesAdded am.S
- StatesRemoved am.S
- StatesTouched am.S
-}
-
-type nodeRef struct {
- // node is a state (reference or top level)
- stateName string
- // node is a state reference, not a top level state
- // eg Bar in case of: Foo -> Remove -> Bar
- // TODO name collision with nodeRef
- isRef bool
- // node is a relation (Remove, Add, Require, After)
- isRel bool
- // relation type (if isRel)
- rel am.Relation
- // top level state name (for both rels and refs)
- parentState string
-}
-
-// ScrollToStateTx scrolls to the next transition involving the state being
-// activated or deactivated. If fwd is true, it scrolls forward, otherwise
-// backwards.
-func (d *Debugger) ScrollToStateTx(state string, fwd bool) {
- step := -1
- if fwd {
- step = 1
- }
- for i := d.cursorTx + step; i > 0 && i < len(d.msgTxs)+1; i = i + step {
- parsed := d.msgTxsParsed[i-1]
- if !lo.Contains(parsed.StatesAdded, state) &&
- !lo.Contains(parsed.StatesRemoved, state) {
- continue
- }
- // scroll to this tx
- d.cursorTx = i
- d.FullRedraw()
- break
- }
-}
-
-func (d *Debugger) Clear() {
- d.treeRoot.ClearChildren()
- d.log.Clear()
- d.msgStruct = nil
- d.msgTxs = []*telemetry.MsgTx{}
- d.Mach.Add(am.S{ss.LiveView}, nil)
-}
-
-func (d *Debugger) FullRedraw() {
- d.updateTree()
- d.updateLog()
- d.updateTimelines()
- d.updateTxBars()
- d.updateKeyBars()
- d.draw()
-}
-
-func (d *Debugger) NextTx() *telemetry.MsgTx {
- onLastTx := d.cursorTx >= len(d.msgTxs)
- if onLastTx {
- return nil
- }
- return d.msgTxs[d.cursorTx]
-}
-
-func (d *Debugger) CurrentTx() *telemetry.MsgTx {
- if d.cursorTx == 0 {
- return nil
- }
- return d.msgTxs[d.cursorTx-1]
-}
-
-func (d *Debugger) initUIComponents() {
- // tree view
- d.tree = d.initMachineTree()
- d.helpScreen = d.initHelpScreen()
- d.tree.SetTitle(" Machine ")
- d.tree.SetBorder(true)
- // log view
- d.log = cview.NewTextView()
- d.log.SetBorder(true)
- d.log.SetRegions(true)
- d.log.SetTextAlign(cview.AlignLeft)
- d.log.SetDynamicColors(true)
- d.log.SetTitle(" Log ")
- // current tx bar
- d.currTxBarLeft = cview.NewTextView()
- d.currTxBarLeft.SetDynamicColors(true)
- d.currTxBarRight = cview.NewTextView()
- d.currTxBarRight.SetTextAlign(cview.AlignRight)
- // next tx bar
- d.nextTxBarLeft = cview.NewTextView()
- d.nextTxBarLeft.SetDynamicColors(true)
- d.nextTxBarRight = cview.NewTextView()
- d.nextTxBarRight.SetTextAlign(cview.AlignRight)
- // TODO step info bar: type, from, to, data
- // timeline tx
- d.timelineTxs = cview.NewProgressBar()
- d.timelineTxs.SetBorder(true)
- // timeline steps
- d.timelineSteps = cview.NewProgressBar()
- d.timelineSteps.SetBorder(true)
- // keystrokes bar
- d.keystrokesBar = cview.NewTextView()
- d.keystrokesBar.SetTextAlign(cview.AlignCenter)
- d.keystrokesBar.SetDynamicColors(true)
-
- // update models
- d.updateTimelines()
- d.updateTxBars()
- d.updateKeyBars()
- // collect all focusable components
- d.focusable = []*cview.Box{
- d.log.Box, d.tree.Box, d.timelineTxs.Box,
- d.timelineSteps.Box,
- }
-}
-
-func (d *Debugger) initLayout() {
- // TODO flexbox
- currTxBar := cview.NewGrid()
- currTxBar.AddItem(d.currTxBarLeft, 0, 0, 1, 1, 0, 0, false)
- currTxBar.AddItem(d.currTxBarRight, 0, 1, 1, 1, 0, 0, false)
-
- // TODO flexbox
- nextTxBar := cview.NewGrid()
- nextTxBar.AddItem(d.nextTxBarLeft, 0, 0, 1, 1, 0, 0, false)
- nextTxBar.AddItem(d.nextTxBarRight, 0, 1, 1, 1, 0, 0, false)
-
- mainGrid := cview.NewGrid()
- mainGrid.SetRows(-1, 2, 3, 2, 3, 2)
- mainGrid.SetColumns(-1, -1, -1)
- mainGrid.AddItem(d.tree, 0, 0, 1, 1, 0, 0, false)
- mainGrid.AddItem(d.log, 0, 1, 1, 2, 0, 0, false)
- mainGrid.AddItem(currTxBar, 1, 0, 1, 3, 0, 0, false)
- mainGrid.AddItem(d.timelineTxs, 2, 0, 1, 3, 0, 0, false)
- mainGrid.AddItem(nextTxBar, 3, 0, 1, 3, 0, 0, false)
- mainGrid.AddItem(d.timelineSteps, 4, 0, 1, 3, 0, 0, false)
- mainGrid.AddItem(d.keystrokesBar, 5, 0, 1, 3, 0, 0, false)
-
- panels := cview.NewPanels()
- panels.AddPanel("help", d.helpScreen, true, true)
- panels.AddPanel("main", mainGrid, true, true)
-
- d.layoutRoot = panels
-}
-
-// TODO better approach to modals
-// TODO modal titles
-func (d *Debugger) initHelpScreen() *cview.Flex {
- left := cview.NewTextView()
- left.SetBackgroundColor(colorHighlight)
- left.SetTitle(" Legend ")
- left.SetDynamicColors(true)
- left.SetPadding(1, 1, 1, 1)
- left.SetText(dedent.Dedent(strings.Trim(`
- [::b]*[::-] handler
- [::b]>[::-] rel source
- [::b]<[::-] rel target
- [::b]+[::-] add
- [::b]-[::-] remove
- [::b]bold[::-] touched
- [::b]underline[::-] requested
- [::b]![::-] cancelled
- `, "\n ")))
-
- right := cview.NewTextView()
- right.SetBackgroundColor(colorHighlight)
- right.SetTitle(" Keystrokes ")
- right.SetDynamicColors(true)
- right.SetPadding(1, 1, 1, 1)
- right.SetText(dedent.Dedent(strings.Trim(`
- [::b]Space[::-] play/pause
- [::b]Left/Right[::-] rewind/fwd
- [::b]Alt+Left/Right[::-] state jump
- [::b]Alt+e[::-] expand/collapse
- [::b]Alt+l[::-] live view
- [::b]Tab[::-] change focus focus
- [::b]Shift+Tab[::-] change focus focus
- [::b]?[::-] show help
- `, "\n ")))
-
- grid := cview.NewGrid()
- grid.SetTitle(" AsyncMachine Debugger ")
- grid.SetColumns(0, 0)
- grid.SetRows(0)
- grid.AddItem(left, 0, 0, 1, 1, 0, 0, false)
- grid.AddItem(right, 0, 1, 1, 1, 0, 0, false)
-
- box1 := cview.NewBox()
- box1.SetBackgroundTransparent(true)
- box2 := cview.NewBox()
- box2.SetBackgroundTransparent(true)
- box3 := cview.NewBox()
- box3.SetBackgroundTransparent(true)
- box4 := cview.NewBox()
- box4.SetBackgroundTransparent(true)
-
- flexHor := cview.NewFlex()
- flexHor.AddItem(box1, 0, 1, false)
- flexHor.AddItem(grid, 0, 1, false)
- flexHor.AddItem(box2, 0, 1, false)
-
- flexVer := cview.NewFlex()
- flexVer.SetDirection(cview.FlexRow)
- flexVer.AddItem(box3, 0, 1, false)
- flexVer.AddItem(flexHor, 0, 1, false)
- flexVer.AddItem(box4, 0, 1, false)
-
- return flexVer
-}
-
-func (d *Debugger) bindKeyboard() {
- // focus manager
- focusManager := cview.NewFocusManager(d.app.SetFocus)
- focusManager.SetWrapAround(true)
- focusManager.Add(d.tree, d.log, d.timelineTxs, d.timelineSteps)
- d.app.SetAfterFocusFunc(func(p cview.Primitive) {
- switch p {
- case d.tree:
- d.Mach.Add(am.S{ss.TreeFocused}, nil)
- case d.log:
- d.Mach.Add(am.S{ss.LogFocused}, nil)
- case d.timelineTxs:
- d.Mach.Add(am.S{ss.TimelineTxsFocused}, nil)
- case d.timelineSteps:
- d.Mach.Add(am.S{ss.TimelineStepsFocused}, nil)
- }
- // update the log highlight on focus change
- d.updateLog()
- })
- inputHandler := cbind.NewConfiguration()
- wrap := func(f func()) func(ev *tcell.EventKey) *tcell.EventKey {
- return func(ev *tcell.EventKey) *tcell.EventKey {
- f()
- return nil
- }
- }
- for _, key := range cview.Keys.MovePreviousField {
- err := inputHandler.Set(key, wrap(focusManager.FocusPrevious))
- if err != nil {
- d.Mach.AddErr(err)
- }
- }
- for _, key := range cview.Keys.MoveNextField {
- err := inputHandler.Set(key, wrap(focusManager.FocusNext))
- if err != nil {
- d.Mach.AddErr(err)
- }
- }
- for key, f := range d.getKeystrokes() {
- err := inputHandler.Set(key, wrap(f))
- if err != nil {
- d.Mach.AddErr(err)
- }
- }
-
- d.app.SetInputCapture(inputHandler.Capture)
-}
-
-func (d *Debugger) getKeystrokes() map[string]func() {
- return map[string]func(){
- // play/pause
- "space": func() {
- if d.Mach.Is(am.S{ss.Playing}) {
- d.Mach.Add(am.S{ss.Paused}, nil)
- } else {
- d.Mach.Add(am.S{ss.Playing}, nil)
- }
- },
- // prev tx
- "Left": func() {
- // TODO fast jump scroll while holding the key
- d.Mach.Remove(am.S{ss.Playing}, nil)
- if d.Mach.Is(am.S{ss.TimelineStepsFocused}) {
- d.Mach.Add(am.S{ss.RewindStep}, nil)
- } else {
- d.Mach.Add(am.S{ss.Rewind}, nil)
- }
- },
- // next tx
- "Right": func() {
- // TODO fast jump scroll while holding the key
- d.Mach.Remove(am.S{ss.Playing}, nil)
- if d.Mach.Is(am.S{ss.TimelineStepsFocused}) {
- d.Mach.Add(am.S{ss.FwdStep}, nil)
- } else {
- d.Mach.Add(am.S{ss.Fwd}, nil)
- }
- },
- // state jump back
- "alt+Left": func() {
- if d.Mach.Is(am.S{ss.StateNameSelected}) {
- d.Mach.Remove(am.S{ss.Playing}, nil)
- d.ScrollToStateTx(d.selectedState, false)
- }
- },
- // state jump fwd
- "alt+Right": func() {
- if d.Mach.Is(am.S{ss.StateNameSelected}) {
- d.Mach.Remove(am.S{ss.Playing}, nil)
- d.ScrollToStateTx(d.selectedState, true)
- }
- },
- // expand / collapse the tree root
- "alt+e": func() {
- expanded := false
- children := d.tree.GetRoot().GetChildren()
- for _, child := range children {
- if child.IsExpanded() {
- expanded = true
- break
- }
- child.Collapse()
- }
- for _, child := range children {
- if expanded {
- child.Collapse()
- } else {
- child.Expand()
- }
- }
- },
- // live view
- "alt+l": func() {
- d.Mach.Add(am.S{ss.LiveView}, nil)
- },
- // scroll to the first tx
- "home": func() {
- d.cursorTx = 0
- d.FullRedraw()
- },
- // scroll to the last tx
- "end": func() {
- d.cursorTx = len(d.msgTxs)
- d.FullRedraw()
- },
- // quit the app
- "ctrl+q": func() {
- d.Mach.Remove(am.S{ss.Init}, nil)
- },
- // help modal
- "?": func() {
- name, _ := d.layoutRoot.GetFrontPanel()
- if name == "main" {
- d.layoutRoot.SendToFront("help")
- } else {
- d.layoutRoot.SendToFront("main")
- }
- },
- // help modal exit
- "esc": func() {
- name, _ := d.layoutRoot.GetFrontPanel()
- if name == "help" {
- d.layoutRoot.SendToFront("main")
- }
- },
- }
-}
-
-func (d *Debugger) parseClientMsg(msgTx *telemetry.MsgTx) {
- msgTxParsed := MsgTxParsed{
- Time: time.Now(),
- }
- // added / removed
- if len(d.msgTxs) > 1 {
- prev := d.msgTxs[len(d.msgTxs)-2]
- for i, name := range d.msgStruct.StatesIndex {
- if prev.StatesActive[i] && !msgTx.StatesActive[i] {
- msgTxParsed.StatesRemoved = append(msgTxParsed.StatesRemoved, name)
- } else if !prev.StatesActive[i] && msgTx.StatesActive[i] {
- msgTxParsed.StatesAdded = append(msgTxParsed.StatesAdded, name)
- } else if prev.Clocks[i] != msgTx.Clocks[i] {
- // treat multi states as added
- msgTxParsed.StatesAdded = append(msgTxParsed.StatesAdded, name)
- }
- }
- }
- touched := am.S{}
- for _, step := range msgTx.Steps {
- if step.FromState != "" {
- touched = append(touched, step.FromState)
- }
- if step.ToState != "" {
- touched = append(touched, step.ToState)
- }
- }
- msgTxParsed.StatesTouched = lo.Uniq(touched)
- d.msgTxsParsed = append(d.msgTxsParsed, msgTxParsed)
-}
-
-func (d *Debugger) appendLog(msgTx *telemetry.MsgTx) error {
- logStr := formatLogEntry(strings.Join(msgTx.PreLogEntries, "\n"))
- if len(logStr) > 0 {
- logStr += "\n"
- }
- if len(msgTx.LogEntries) > 0 {
- logStr += `["` + msgTx.ID + `"]` +
- formatLogEntry(strings.Join(msgTx.LogEntries, "\n")) +
- `[""]` + "\n"
- }
- if len(logStr) == 0 {
- return nil
- }
- _, err := d.log.Write([]byte(logStr))
- if err != nil {
- return err
- }
- return nil
-}
-
-func (d *Debugger) handleMsgStruct(msg *telemetry.MsgStruct) {
- d.treeRoot.SetText(msg.ID)
- d.msgStruct = msg
- d.treeRoot.ClearChildren()
- for _, name := range msg.StatesIndex {
- d.addState(name)
- }
-}
-
-func (d *Debugger) draw() {
- // TODO debounce every 16msec
- d.app.QueueUpdateDraw(func() {})
-}
-
-func (d *Debugger) updateTxBars() {
- d.currTxBarLeft.Clear()
- d.currTxBarRight.Clear()
- d.nextTxBarLeft.Clear()
- d.nextTxBarRight.Clear()
- tx := d.CurrentTx()
- if len(d.msgTxs) == 0 {
- d.currTxBarLeft.SetText("Waiting for a client...")
- } else if tx == nil {
- d.currTxBarLeft.SetText(formatTxBarTitle("Paused"))
- } else {
- var title string
- switch d.Mach.Switch(ss.GroupPlaying...) {
- case ss.Playing:
- title = "Playing"
- case ss.Paused:
- title = "Paused"
- case ss.LiveView:
- title += "Live"
- if d.Mach.Not(am.S{ss.ClientConnected}) {
- title += " (disconnected)"
- }
- }
- left, right := getTxInfo(tx, &d.msgTxsParsed[d.cursorTx-1], title)
- d.currTxBarLeft.SetText(left)
- d.currTxBarRight.SetText(right)
- }
-
- nextTx := d.NextTx()
- if nextTx != nil {
- title := "Next"
- left, right := getTxInfo(nextTx, &d.msgTxsParsed[d.cursorTx], title)
- d.nextTxBarLeft.SetText(left)
- d.nextTxBarRight.SetText(right)
- }
-}
-
-func (d *Debugger) updateLog() {
- if d.msgStruct != nil {
- lvl := d.msgStruct.LogLevel
- d.log.SetTitle(" Log:" + lvl.String() + " ")
- }
- // highlight the next tx if scrolling by steps
- bySteps := d.Mach.Is(am.S{ss.TimelineStepsFocused})
- tx := d.CurrentTx()
- if bySteps {
- tx = d.NextTx()
- }
- if tx == nil {
- d.log.Highlight("")
- if bySteps {
- d.log.ScrollToEnd()
- } else {
- d.log.ScrollToBeginning()
- }
- return
- }
- d.log.Highlight(tx.ID)
- d.log.ScrollToHighlight()
-}
-
-func (d *Debugger) updateTimelines() {
- txCount := len(d.msgTxs)
- nextTx := d.NextTx()
- d.timelineSteps.SetTitleColor(cview.Styles.PrimaryTextColor)
- d.timelineSteps.SetBorderColor(cview.Styles.PrimaryTextColor)
- d.timelineSteps.SetFilledColor(cview.Styles.PrimaryTextColor)
- // mark last step of a cancelled tx in red
- if nextTx != nil && d.cursorStep == len(nextTx.Steps) && !nextTx.Accepted {
- d.timelineSteps.SetFilledColor(tcell.ColorRed)
- }
- // inactive steps bar when no next tx
- if nextTx == nil {
- d.timelineSteps.SetTitleColor(tcell.ColorGrey)
- d.timelineSteps.SetBorderColor(tcell.ColorGrey)
- }
- stepsCount := 0
- onLastTx := d.cursorTx >= txCount
- if !onLastTx {
- stepsCount = len(d.msgTxs[d.cursorTx].Steps)
- }
-
- // progressbar cant be max==0
- d.timelineTxs.SetMax(max(txCount, 1))
- // progress <= max
- d.timelineTxs.SetProgress(d.cursorTx)
- d.timelineTxs.SetTitle(fmt.Sprintf(
- " Transition %d / %d ", d.cursorTx, txCount))
-
- // progressbar cant be max==0
- d.timelineSteps.SetMax(max(stepsCount, 1))
- // progress <= max
- d.timelineSteps.SetProgress(d.cursorStep)
- d.timelineSteps.SetTitle(fmt.Sprintf(
- " Next mutation step %d / %d ", d.cursorStep, stepsCount))
-}
-
-func (d *Debugger) addState(name string) {
- state := d.msgStruct.States[name]
- stateNode := cview.NewTreeNode(name + " (0)")
- stateNode.SetReference(name)
- stateNode.SetSelectable(true)
- stateNode.SetReference(nodeRef{stateName: name})
- d.treeRoot.AddChild(stateNode)
- stateNode.SetColor(colorInactive)
- // labels
- labels := ""
- if state.Auto {
- labels += "auto"
- }
- if state.Multi {
- if labels != "" {
- labels += " "
- }
- labels += "multi"
- }
- if labels != "" {
- labelNode := cview.NewTreeNode(labels)
- labelNode.SetSelectable(false)
- stateNode.AddChild(labelNode)
- }
- // relations
- addRelation(stateNode, name, am.RelationAdd, state.Add)
- addRelation(stateNode, name, am.RelationRequire, state.Require)
- addRelation(stateNode, name, am.RelationRemove, state.Remove)
- addRelation(stateNode, name, am.RelationAfter, state.After)
-}
-
-func (d *Debugger) updateKeyBars() {
- // TODO light mode
- stateJump := "[grey] | Alt+Left/Right: state jump[yellow]"
- if d.Mach.Is(am.S{ss.StateNameSelected}) {
- stateJump = " | Alt+Left/Right: state jump"
- }
- keys := "[yellow]Space: play/pause | Left/Right: rewind/fwd | Alt+e: " +
- "expand/collapse | Tab: focus | Alt+l: live view | ?: help" + stateJump
- d.keystrokesBar.SetText(keys)
-}
-
-func formatLogEntry(entry string) string {
- entry = strings.ReplaceAll(strings.ReplaceAll(entry,
- "[", "{{{"),
- "]", "}}}")
- // TODO light mode
- entry = strings.ReplaceAll(strings.ReplaceAll(entry,
- "{{{", "[yellow]["),
- "}}}", "[][white]")
- return entry
-}
-
-func getTxInfo(
- tx *telemetry.MsgTx, parsed *MsgTxParsed, title string,
-) (string, string) {
- // left side
- left := formatTxBarTitle(title)
- if tx != nil {
- left += " | " + tx.ID
- if tx.IsAuto {
- left += " | auto"
- }
- if tx.Accepted {
- left += " | accepted"
- } else if !tx.Accepted {
- left += " | rejected"
- }
- }
- // right side
- right := fmt.Sprintf("(A: %d | R: %d | T: %d) %s",
- len(parsed.StatesAdded), len(parsed.StatesRemoved),
- len(parsed.StatesTouched), parsed.Time.Format(time.DateTime),
- )
- return left, right
-}
-
-func formatTxBarTitle(title string) string {
- return "[::u]" + title + "[::-]"
-}
diff --git a/tools/am-dbg/debugger/handlers.go b/tools/am-dbg/debugger/handlers.go
deleted file mode 100644
index bcd4d0c..0000000
--- a/tools/am-dbg/debugger/handlers.go
+++ /dev/null
@@ -1,218 +0,0 @@
-// TODO ExceptionState: separate error screen with stack trace
-
-package debugger
-
-import (
- "time"
-
- "github.com/pancsta/cview"
-
- am "github.com/pancsta/asyncmachine-go/pkg/machine"
- "github.com/pancsta/asyncmachine-go/pkg/telemetry"
- ss "github.com/pancsta/asyncmachine-go/tools/am-dbg/states"
-)
-
-func (d *Debugger) InitState(_ *am.Event) {
- d.app = cview.NewApplication()
- d.initUIComponents()
- d.initLayout()
- d.bindKeyboard()
- if d.EnableMouse {
- d.app.EnableMouse(true)
- }
-
- // redraw on auto states
- go func() {
- // bind to transitions
- txEndCh := d.Mach.On([]string{am.EventTransitionEnd}, nil)
- for event := range txEndCh {
- // TODO typesafe Args
- tx := event.Args["transition"].(*am.Transition)
- if tx.IsAuto() && tx.Accepted {
- d.updateTxBars()
- d.draw()
- }
- }
- }()
-
- // draw in a goroutine
- go func() {
- d.app.SetRoot(d.layoutRoot, true)
- d.app.SetFocus(d.tree)
- err := d.app.Run()
- if err != nil {
- d.Mach.AddErr(err)
- }
- d.Mach.Remove(am.S{"Init"}, nil)
- }()
-}
-
-func (d *Debugger) StateNameSelectedState(e *am.Event) {
- d.selectedState = e.Args["selectedStateName"].(string)
- d.updateTree()
- d.updateKeyBars()
-}
-
-func (d *Debugger) StateNameSelectedStateNameSelected(e *am.Event) {
- d.StateNameSelectedState(e)
-}
-
-func (d *Debugger) StateNameSelectedEnd(_ *am.Event) {
- d.selectedState = ""
- d.updateTree()
- d.updateKeyBars()
-}
-
-func (d *Debugger) LiveViewState(_ *am.Event) {
- if d.cursorTx == 0 {
- return
- }
- d.cursorTx = len(d.msgTxs)
- d.FullRedraw()
-}
-
-func (d *Debugger) PlayingState(_ *am.Event) {
- // TODO play by steps
- if d.playTimer == nil {
- d.playTimer = time.NewTicker(playInterval)
- } else {
- d.playTimer.Reset(time.Second)
- }
- ctx := d.Mach.GetStateCtx(ss.Playing)
- go func() {
- d.Mach.Add(am.S{ss.Fwd}, nil)
- for range d.playTimer.C {
- if ctx.Err() != nil {
- break
- }
- d.Mach.Add(am.S{ss.Fwd}, nil)
- }
- }()
-}
-
-func (d *Debugger) PlayingEnd(_ *am.Event) {
- d.playTimer.Stop()
-}
-
-func (d *Debugger) FwdEnter(_ *am.Event) bool {
- return d.cursorTx < len(d.msgTxs)
-}
-
-func (d *Debugger) FwdState(_ *am.Event) {
- defer d.Mach.Remove(am.S{ss.Fwd}, nil)
- d.cursorTx++
- d.cursorStep = 0
- if d.Mach.Is(am.S{ss.Playing}) && d.cursorTx == len(d.msgTxs) {
- d.Mach.Remove(am.S{ss.Playing}, nil)
- }
- d.FullRedraw()
-}
-
-func (d *Debugger) RewindEnter(_ *am.Event) bool {
- return d.cursorTx > 0
-}
-
-func (d *Debugger) RewindState(_ *am.Event) {
- defer d.Mach.Remove(am.S{ss.Rewind}, nil)
- d.cursorTx--
- d.cursorStep = 0
- d.FullRedraw()
-}
-
-func (d *Debugger) FwdStepEnter(_ *am.Event) bool {
- nextTx := d.NextTx()
- if nextTx == nil {
- return false
- }
- return d.cursorStep < len(nextTx.Steps)+1
-}
-
-func (d *Debugger) FwdStepState(_ *am.Event) {
- defer d.Mach.Remove(am.S{ss.FwdStep}, nil)
- // next tx
- nextTx := d.NextTx()
- // scroll to the next tx
- if d.cursorStep == len(nextTx.Steps) {
- d.Mach.Add(am.S{ss.Fwd}, nil)
- return
- }
- d.cursorStep++
- d.FullRedraw()
-}
-
-func (d *Debugger) RewindStepEnter(_ *am.Event) bool {
- return d.cursorStep > 0 || d.cursorTx > 0
-}
-
-func (d *Debugger) RewindStepState(_ *am.Event) {
- defer d.Mach.Remove(am.S{ss.RewindStep}, nil)
- // wrap if theres a prev tx
- if d.cursorStep <= 0 {
- d.cursorTx--
- d.updateLog()
- nextTx := d.NextTx()
- d.cursorStep = len(nextTx.Steps)
- } else {
- d.cursorStep--
- }
- d.FullRedraw()
-}
-
-func (d *Debugger) HelpScreenState() {
- if d.helpScreen == nil {
- d.helpScreen = d.initHelpScreen()
- }
- d.treeRoot.ClearChildren()
- d.log.Clear()
- d.msgStruct = nil
- d.msgTxs = []*telemetry.MsgTx{}
- d.Mach.Add(am.S{ss.LiveView}, nil)
-}
-
-func (d *Debugger) ClientConnectedState(_ *am.Event) {
- d.Clear()
- for _, box := range d.focusable {
- box.SetBorderColorFocused(colorActive)
- }
- d.draw()
-}
-
-func (d *Debugger) ClientConnectedEnd(_ *am.Event) {
- for _, box := range d.focusable {
- box.SetBorderColorFocused(cview.ColorUnset)
- }
- d.Mach.Remove(am.S{ss.LiveView}, nil)
- d.draw()
-}
-
-func (d *Debugger) ClientMsgState(e *am.Event) {
- // initial structure data
- _, structOk := e.Args["msg_struct"]
- if structOk {
- msgStruct := e.Args["msg_struct"].(*telemetry.MsgStruct)
- d.handleMsgStruct(msgStruct)
- return
- }
- // transition data
- msgTx := e.Args["msg_tx"].(*telemetry.MsgTx)
- // TODO scalable storage
- d.msgTxs = append(d.msgTxs, msgTx)
- // parse the msg
- d.parseClientMsg(msgTx)
- err := d.appendLog(msgTx)
- if err != nil {
- d.Mach.AddErr(err)
- return
- }
- if d.Mach.Is(am.S{ss.LiveView}) {
- // force the latest tx
- // TODO debounce?
- d.cursorTx = len(d.msgTxs)
- d.cursorStep = 0
- d.updateTxBars()
- d.updateTree()
- d.updateLog()
- }
- d.updateTimelines()
- d.draw()
-}
diff --git a/tools/am-dbg/debugger/rpc.go b/tools/am-dbg/debugger/rpc.go
deleted file mode 100644
index 709faaa..0000000
--- a/tools/am-dbg/debugger/rpc.go
+++ /dev/null
@@ -1,64 +0,0 @@
-package debugger
-
-import (
- "encoding/gob"
- "net"
- "net/rpc"
-
- am "github.com/pancsta/asyncmachine-go/pkg/machine"
- "github.com/pancsta/asyncmachine-go/pkg/telemetry"
- ss "github.com/pancsta/asyncmachine-go/tools/am-dbg/states"
- "log"
-)
-
-type RPCServer struct {
- Mach *am.Machine
- URL string
-}
-
-func (r *RPCServer) MsgStruct(msgStruct *telemetry.MsgStruct, _ *string) error {
- r.Mach.Add(am.S{ss.ClientMsg}, am.A{"msg_struct": msgStruct})
- return nil
-}
-
-func (r *RPCServer) MsgTx(msgTx *telemetry.MsgTx, _ *string) error {
- r.Mach.Add(am.S{ss.ClientMsg}, am.A{"msg_tx": msgTx})
- return nil
-}
-
-func StartRCP(rcvr *RPCServer) {
- var err error
- gob.Register(am.Relation(0))
- url := telemetry.RpcHost
- if rcvr.URL != "" {
- url = rcvr.URL
- }
- l, err := net.Listen("tcp", url)
- if err != nil {
- rcvr.Mach.AddErr(err)
- // TODO nice err msg
- panic(err)
- }
- log.Println("RPC server started at", url)
- server := rpc.NewServer()
- err = server.Register(rcvr)
- if err != nil {
- rcvr.Mach.AddErr(err)
- }
- for {
- conn, err := l.Accept()
- if err != nil {
- rcvr.Mach.AddErr(err)
- continue
- }
- // only 1 client
- if rcvr.Mach.Is(am.S{ss.ClientConnected}) {
- continue
- }
- rcvr.Mach.Add(am.S{ss.ClientConnected}, nil)
- go func() {
- server.ServeConn(conn)
- rcvr.Mach.Remove(am.S{ss.ClientConnected}, nil)
- }()
- }
-}
diff --git a/tools/am-dbg/debugger/tree.go b/tools/am-dbg/debugger/tree.go
deleted file mode 100644
index 19797b6..0000000
--- a/tools/am-dbg/debugger/tree.go
+++ /dev/null
@@ -1,253 +0,0 @@
-package debugger
-
-import (
- "log"
- "strconv"
- "strings"
-
- "github.com/gdamore/tcell/v2"
- "github.com/pancsta/cview"
-
- "fmt"
- am "github.com/pancsta/asyncmachine-go/pkg/machine"
- "github.com/pancsta/asyncmachine-go/pkg/telemetry"
- ss "github.com/pancsta/asyncmachine-go/tools/am-dbg/states"
-)
-
-func (d *Debugger) initMachineTree() *cview.TreeView {
- d.treeRoot = cview.NewTreeNode("")
- d.treeRoot.SetColor(tcell.ColorRed)
- tree := cview.NewTreeView()
- tree.SetRoot(d.treeRoot)
- tree.SetCurrentNode(d.treeRoot)
- tree.SetHighlightColor(colorHighlight)
- tree.SetChangedFunc(func(node *cview.TreeNode) {
- reference := node.GetReference()
- if reference == nil || reference.(nodeRef).stateName == "" {
- d.Mach.Remove(am.S{ss.StateNameSelected}, nil)
- return
- }
- ref := reference.(nodeRef)
- d.Mach.Add(am.S{ss.StateNameSelected},
- am.A{"selectedStateName": ref.stateName})
- })
- tree.SetSelectedFunc(func(node *cview.TreeNode) {
- node.SetExpanded(!node.IsExpanded())
- })
- return tree
-}
-
-func (d *Debugger) updateTree() {
- var msg telemetry.Msg
- queue := ""
- if d.cursorTx == 0 {
- msg = d.msgStruct
- } else {
- tx := d.msgTxs[d.cursorTx-1]
- msg = tx
- queue = "(" + strconv.Itoa(tx.Queue) + ") "
- }
- d.tree.SetTitle(" Machine " + queue)
- var steps []*am.TransitionStep
- if d.cursorTx < len(d.msgTxs) && d.cursorStep > 0 {
- steps = d.NextTx().Steps
- }
-
- // default decorations plus name highlights
- d.updateTreeDefaultsHighlights(msg)
- // decorate steps
- d.updateTreeTxSteps(steps)
-}
-
-func (d *Debugger) updateTreeDefaultsHighlights(msg telemetry.Msg) {
- d.tree.GetRoot().Walk(func(node, parent *cview.TreeNode) bool {
- // skip the root
- if parent == nil {
- return true
- }
- refSrc := node.GetReference()
- if refSrc == nil {
- return true
- }
- node.SetBold(false)
- node.SetUnderline(false)
- ref := refSrc.(nodeRef)
- if ref.isRel {
- node.SetText(capitalizeFirst(ref.rel.String()))
- return true
- }
- // inherit
- if parent == d.tree.GetRoot() || !parent.GetHighlighted() {
- node.SetHighlighted(false)
- }
- stateName := ref.stateName
- color := colorInactive
- if msg.Is(d.msgStruct.StatesIndex, am.S{stateName}) {
- color = colorActive
- }
- // reset to defaults
- node.SetText(stateName)
- // reset to defaults
- if stateName != d.selectedState {
- if !ref.isRef {
- // un-highlight all descendants
- for _, child := range node.GetChildren() {
- child.SetHighlighted(false)
- for _, child2 := range child.GetChildren() {
- child2.SetHighlighted(false)
- }
- }
- tick := strconv.FormatUint(msg.Clock(d.msgStruct.StatesIndex,
- stateName), 10)
- node.SetColor(color)
- node.SetText(stateName + " (" + tick + ")")
- }
- return true
- }
- // reference
- if node != d.tree.GetCurrentNode() {
- node.SetHighlighted(true)
- log.Println("highlight", stateName)
- }
- if ref.isRef {
- return true
- }
- // top-level state
- tick := strconv.FormatUint(msg.Clock(d.msgStruct.StatesIndex,
- stateName), 10)
- node.SetColor(color)
- node.SetText(stateName + " (" + tick + ")")
- if node == d.tree.GetCurrentNode() {
- return true
- }
- // highlight all descendants
- for _, child := range node.GetChildren() {
- child.SetHighlighted(true)
- for _, child2 := range child.GetChildren() {
- child2.SetHighlighted(true)
- }
- }
- return true
- })
-}
-
-func (d *Debugger) updateTreeTxSteps(steps []*am.TransitionStep) {
- // walk the tree only when scrolling steps
- if d.cursorStep < 1 {
- return
- }
- d.tree.GetRoot().Walk(func(node, parent *cview.TreeNode) bool {
- // skip the root
- if parent == nil {
- return true
- }
- refSrc := node.GetReference()
- if refSrc == nil {
- return true
- }
- ref := refSrc.(nodeRef)
- if ref.stateName != "" {
- // STATE NAME NODES
- stateName := ref.stateName
- for i := range steps {
- if d.cursorStep == i {
- break
- }
- step := steps[i]
- switch step.Type {
- case am.TransitionStepTypeNoSet:
- if step.ToState == stateName && !ref.isRef {
- node.SetBold(true)
- }
- case am.TransitionStepTypeRemove:
- if step.ToState == stateName && !ref.isRef {
- node.SetText("-" + node.GetText())
- node.SetBold(true)
- }
- case am.TransitionStepTypeRelation:
- if step.FromState == stateName && !ref.isRef {
- node.SetBold(true)
- } else if step.ToState == stateName && !ref.isRef {
- node.SetText(fmt.Sprintf("%s <%d", node.GetText(), i+1))
- node.SetBold(true)
- } else if ref.isRef && step.ToState == stateName &&
- ref.parentState == step.FromState {
- node.SetText(fmt.Sprintf("%s >%d", node.GetText(), i+1))
- node.SetBold(true)
- }
- case am.TransitionStepTypeTransition:
- if ref.isRef {
- continue
- }
- // states handler executed
- if step.FromState == stateName || step.ToState == stateName {
- node.SetText("*" + node.GetText())
- node.SetBold(true)
- }
- case am.TransitionStepTypeSet:
- if step.ToState == stateName && !ref.isRef {
- node.SetText("+" + node.GetText())
- node.SetBold(true)
- }
- case am.TransitionStepTypeRequested:
- if step.ToState == stateName && !ref.isRef {
- node.SetUnderline(true)
- node.SetBold(true)
- }
- case am.TransitionStepTypeCancel:
- if step.ToState == stateName && !ref.isRef {
- node.SetText("!" + node.GetText())
- node.SetBold(true)
- }
- }
- }
- } else if ref.isRel {
- // RELATION NODES
- for i := range steps {
- if d.cursorStep == i {
- break
- }
- step := steps[i]
- if step.Type != am.TransitionStepTypeRelation {
- continue
- }
- if step.Data == ref.rel && ref.parentState == step.FromState {
- node.SetBold(true)
- }
- }
- }
- return true
- })
-}
-
-func addRelation(
- stateNode *cview.TreeNode, name string, rel am.Relation, relations []string,
-) {
- if len(relations) <= 0 {
- return
- }
- relNode := cview.NewTreeNode(capitalizeFirst(rel.String()))
- relNode.SetSelectable(true)
- relNode.SetReference(nodeRef{
- isRel: true,
- rel: rel,
- parentState: name,
- })
- for _, relState := range relations {
- stateNode := cview.NewTreeNode(relState)
- stateNode.SetReference(nodeRef{
- isRef: true,
- stateName: relState,
- parentState: name,
- })
- relNode.AddChild(stateNode)
- }
- stateNode.AddChild(relNode)
-}
-
-func capitalizeFirst(s string) string {
- if len(s) == 0 {
- return s
- }
- return strings.ToUpper(string(s[0])) + s[1:]
-}
diff --git a/tools/am-dbg/main.go b/tools/am-dbg/main.go
deleted file mode 100644
index 6bad89e..0000000
--- a/tools/am-dbg/main.go
+++ /dev/null
@@ -1,154 +0,0 @@
-package main
-
-import (
- "context"
- "fmt"
- "log"
- "os"
- "runtime/debug"
- "time"
-
- "github.com/spf13/cobra"
-
- am "github.com/pancsta/asyncmachine-go/pkg/machine"
- "github.com/pancsta/asyncmachine-go/pkg/telemetry"
- "github.com/pancsta/asyncmachine-go/tools/am-dbg/debugger"
- ss "github.com/pancsta/asyncmachine-go/tools/am-dbg/states"
-)
-
-const (
- cliParamLogFile string = "log-file"
- cliParamLogLevel string = "log-level"
- cliParamLogID string = "log-machine-id"
- cliParamServerURL string = "server-url"
- cliParamAmDbgURL string = "am-dbg-url"
- cliParamEnableMouse string = "enable-mouse"
- cliParamVersion string = "version"
-)
-
-func main() {
- rootCmd := getRootCmd()
- err := rootCmd.Execute()
- if err != nil {
- panic(err)
- }
-}
-
-func getRootCmd() *cobra.Command {
- rootCmd := &cobra.Command{
- Use: "am-dbg",
- Run: cliRun,
- }
- // TODO --dark-mode auto|on|off
- rootCmd.Flags().String(cliParamLogFile, "am-dbg.log",
- "Log file path")
- rootCmd.Flags().Int(cliParamLogLevel, 0,
- "Log level, 0-5 (silent-everything)")
- rootCmd.Flags().Bool(cliParamLogID, true,
- "Include machine ID in log messages")
- rootCmd.Flags().String(cliParamServerURL, telemetry.RpcHost,
- "Host and port for the server to listen on")
- rootCmd.Flags().String(cliParamAmDbgURL, "",
- "Debug this instance of am-dbg with another one")
- rootCmd.Flags().Bool(cliParamEnableMouse, false,
- "Enable mouse support")
- rootCmd.Flags().Bool(cliParamVersion, false,
- "Print version and exit")
- return rootCmd
-}
-
-func cliRun(cmd *cobra.Command, _ []string) {
- // params
- version, err := cmd.Flags().GetBool(cliParamVersion)
- if err != nil {
- panic(err)
- }
- logFile := cmd.Flag(cliParamLogFile).Value.String()
- logLevelInt, err := cmd.Flags().GetInt(cliParamLogLevel)
- if err != nil {
- panic(err)
- }
- logLevel := am.LogLevel(logLevelInt)
- serverURL := cmd.Flag(cliParamServerURL).Value.String()
- debugURL := cmd.Flag(cliParamAmDbgURL).Value.String()
- enableMouse, err := cmd.Flags().GetBool(cliParamEnableMouse)
- if err != nil {
- panic(err)
- }
-
- if version {
- build, ok := debug.ReadBuildInfo()
- if !ok {
- panic("No build info available")
- }
- fmt.Println(build.Main.Version)
- os.Exit(0)
- }
-
- // file logging
- if logLevel > 0 && logFile != "" {
- _ = os.Remove(logFile)
- file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY, 0o666)
- if err != nil {
- panic(err)
- }
- defer file.Close()
- log.SetOutput(file)
- }
-
- mach, err := initMachine(logLevel, enableMouse)
- if err != nil {
- panic(err)
- }
-
- // rpc client
- if debugURL != "" {
- err := telemetry.MonitorTransitions(mach, debugURL)
- // TODO retries
- if err != nil {
- panic(err.Error())
- }
- }
-
- // rpc server
- go debugger.StartRCP(&debugger.RPCServer{
- Mach: mach,
- URL: serverURL,
- })
-
- // start and wait till the end
- mach.Add(am.S{"Init"}, nil)
- <-mach.WhenNot(am.S{"Init"}, nil)
-}
-
-func initMachine(logLevel am.LogLevel, enableMouse bool) (*am.Machine, error) {
- // machine
- ctx := context.Background()
- mach := am.New(ctx, ss.States, &am.Opts{
- HandlerTimeout: time.Hour,
- DontPanicToException: true,
- })
- err := mach.VerifyStates(ss.Names)
- if err != nil {
- return nil, err
- }
-
- mach.SetLogLevel(logLevel)
- mach.LogID = false
- mach.SetLogger(func(level am.LogLevel, msg string, args ...any) {
- txt := fmt.Sprintf(msg, args...)
- log.Print(txt)
- })
-
- // init handlers
- h := &debugger.Debugger{
- Mach: mach,
- EnableMouse: enableMouse,
- }
- err = mach.BindHandlers(h)
- if err != nil {
- return nil, err
- }
-
- return mach, nil
-}
diff --git a/tools/am-dbg/states/states.go b/tools/am-dbg/states/states.go
deleted file mode 100644
index 9ba7b73..0000000
--- a/tools/am-dbg/states/states.go
+++ /dev/null
@@ -1,92 +0,0 @@
-package states
-
-import am "github.com/pancsta/asyncmachine-go/pkg/machine"
-
-// enum of all the state names
-const (
- TreeFocused string = "TreeFocused"
- LogFocused string = "LogFocused"
- TimelineTxsFocused string = "TimelineTxsFocused"
- TimelineStepsFocused string = "TimelineStepsFocused"
- ClientMsg string = "ClientMsg"
- KeystrokeInput string = "KeystrokeInput"
- Paused string = "Paused"
- StateNameSelected string = "StateNameSelected"
- Init string = "Init"
- Playing string = "Playing"
- Fwd string = "Fwd"
- Rewind string = "Rewind"
- FwdStep string = "FwdStep"
- RewindStep string = "RewindStep"
- ClientConnected string = "ClientConnected"
- LiveView string = "LiveView"
- HelpScreen string = "HelpScreen"
- Exception string = "Exception"
-)
-
-var Names = am.S{
- ClientMsg, KeystrokeInput, TreeFocused, LogFocused, TimelineTxsFocused,
- TimelineStepsFocused, StateNameSelected, ClientConnected, Init, LiveView,
- Playing, Paused, Fwd, Rewind, FwdStep, RewindStep, HelpScreen, Exception,
-}
-
-var groupFocused = am.S{
- TreeFocused, LogFocused, TimelineTxsFocused,
- TimelineStepsFocused,
-}
-
-var GroupPlaying = am.S{
- Playing, Paused, LiveView,
-}
-
-var States = am.States{
- // Input events
- ClientMsg: {
- Multi: true,
- },
- KeystrokeInput: {
- Multi: true,
- },
-
- // State (external)
- TreeFocused: {
- Remove: groupFocused,
- },
- LogFocused: {
- Remove: groupFocused,
- },
- TimelineTxsFocused: {
- Remove: groupFocused,
- },
- TimelineStepsFocused: {
- Remove: groupFocused,
- },
- StateNameSelected: {
- Multi: true,
- },
- ClientConnected: {},
-
- // Actions
- Init: {
- Add: am.S{LiveView},
- },
- LiveView: {
- Remove: GroupPlaying,
- },
- Playing: {
- Remove: GroupPlaying,
- },
- Paused: {
- Auto: true,
- Remove: GroupPlaying,
- },
- Fwd: {},
- Rewind: {
- Remove: am.S{LiveView},
- },
- FwdStep: {
- Remove: am.S{LiveView},
- },
- RewindStep: {},
- HelpScreen: {},
-}
diff --git a/tools/cmd/am-dbg-demo/main.go b/tools/cmd/am-dbg-demo/main.go
new file mode 100644
index 0000000..7f635eb
--- /dev/null
+++ b/tools/cmd/am-dbg-demo/main.go
@@ -0,0 +1,159 @@
+package main
+
+import (
+ "context"
+ "encoding/gob"
+ "log"
+ "os"
+ "time"
+
+ am "github.com/pancsta/asyncmachine-go/pkg/machine"
+ "github.com/pancsta/asyncmachine-go/tools/debugger"
+ ss "github.com/pancsta/asyncmachine-go/tools/debugger/states"
+)
+
+var (
+ gobFile = "assets/am-dbg-sim.gob.bz2"
+ logLevel = am.LogOps
+ logFile = "am-dbg-demo.log"
+ startupMachine = "sim-p1"
+ startupTx = 25
+ initialView = "matrix"
+ playInterval = 200 * time.Millisecond
+)
+
+func main() {
+ var err error
+
+ // file logging
+ if logFile != "" {
+ _ = os.Remove(logFile)
+ file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY, 0o666)
+ if err != nil {
+ panic(err)
+ }
+ defer file.Close()
+ log.SetOutput(file)
+ }
+
+ // init the debugger
+ // TODO extract to tools/debugger
+ dbg := &debugger.Debugger{
+ Clients: make(map[string]*debugger.Client),
+ }
+ gob.Register(debugger.Exportable{})
+ gob.Register(am.Relation(0))
+
+ // import data
+ dbg.ImportData(gobFile)
+
+ // init machine
+ dbg.Mach, err = initMachine(dbg, logLevel)
+ if err != nil {
+ panic(err)
+ }
+
+ // start and wait till the end
+ dbg.Mach.Add1(ss.Start, am.A{
+ "Client.id": startupMachine,
+ "Client.cursorTx": startupTx,
+ "dbgView": initialView,
+ })
+
+ runDemo(dbg)
+
+ dbg.Mach.Remove1(ss.Start, nil)
+}
+
+// demo:
+// 1 p1, tx: 25, matrix view
+// 2 tx: 45
+// 3 tree matrix view
+// 4 tx: 56
+// 5 step timeline
+// 6 tx: 58
+// 7 tree log view
+// 8 reverse steps
+// 9 tx: 56
+func runDemo(dbg *debugger.Debugger) {
+ mach := dbg.Mach
+ <-mach.When1(ss.Ready, nil)
+ time.Sleep(100 * time.Millisecond)
+
+ // play txes
+ goFwd(mach, 20)
+ mach.Add1(ss.TreeMatrixView, nil)
+ waitForRender()
+ goFwd(mach, 11)
+
+ // play steps
+ goFwdSteps(mach, 8)
+ // highlight Connected
+ mach.Add1(ss.StateNameSelected, am.A{"selectedStateName": "Connected"})
+ waitForRender()
+
+ goFwdSteps(mach, 15)
+ waitForRender()
+
+ // go back
+ mach.Add1(ss.TreeLogView, nil)
+ goBackSteps(mach, 23)
+ waitForRender()
+
+ // end screen
+ time.Sleep(2 * time.Second)
+}
+
+func goFwd(mach *am.Machine, amount int) {
+ for i := 0; i < amount; i++ {
+ time.Sleep(playInterval)
+ mach.Add1(ss.Fwd, nil)
+ }
+}
+
+func goFwdSteps(mach *am.Machine, amount int) {
+ for i := 0; i < amount; i++ {
+ time.Sleep(playInterval)
+ mach.Add1(ss.FwdStep, nil)
+ }
+}
+
+func goBackSteps(mach *am.Machine, amount int) {
+ for i := 0; i < amount; i++ {
+ time.Sleep(playInterval)
+ mach.Add1(ss.BackStep, nil)
+ }
+}
+
+func waitForRender() {
+ time.Sleep(100 * time.Millisecond)
+}
+
+// TODO extract to tools/debugger
+func initMachine(
+ h *debugger.Debugger, logLevel am.LogLevel) (*am.Machine, error) {
+ ctx := context.Background()
+
+ // TODO use NewCommon
+ mach := am.New(ctx, ss.States, &am.Opts{
+ HandlerTimeout: time.Hour,
+ DontPanicToException: true,
+ DontLogID: true,
+ })
+ mach.ID = "am-dbg-" + mach.ID
+
+ err := mach.VerifyStates(ss.Names)
+ if err != nil {
+ return nil, err
+ }
+
+ mach.SetTestLogger(log.Printf, logLevel)
+ mach.SetLogArgs(am.NewArgsMapper([]string{"Machine.id", "conn_id"}, 20))
+
+ err = mach.BindHandlers(h)
+ if err != nil {
+ return nil, err
+ }
+
+ return mach, nil
+}
diff --git a/tools/cmd/am-dbg/README.md b/tools/cmd/am-dbg/README.md
new file mode 100644
index 0000000..2d33edd
--- /dev/null
+++ b/tools/cmd/am-dbg/README.md
@@ -0,0 +1,76 @@
+# am-dbg
+
+`am-dbg` is a multi-client debugger for [asyncmachine-go](https://github.com/pancsta/asyncmachine-go). It's lightweight
+enough to be kept open in the background while receiving data from >100 machines simultaneously (and potentially many
+more).
+
+![TUI Debugger](../../../assets/am-dbg.png)
+
+## Features
+
+- states tree
+- log view
+- time travel
+- transition steps
+- import / export
+- matrix view
+
+```text
+Usage:
+ am-dbg [flags]
+
+Flags:
+ --am-dbg-url string Debug this instance of am-dbg with another one
+ --clean-on-connect Clean up disconnected clients on the 1st connection
+ --enable-mouse Enable mouse support (experimental)
+ -h, --help help for am-dbg
+ -i, --import-data string Import an exported gob.bz2 file
+ -l, --listen-on string Host and port for the debugger to listen on (default "localhost:6831")
+ --log-file string Log file path (default "am-dbg.log")
+ --log-level int Log level, 0-5 (silent-everything)
+ -c, --select-connected Select the newly connected machine, if no other is connected
+ -m, --select-machine string Select a machine by ID on startup (requires --import-data)
+ -t, --select-transition int Select a transaction by _number_ on startup (requires --select-machine)
+ --version Print version and exit
+ -v, --view string Initial view (tree-log, tree-matrix, matrix) (default "tree-log")
+```
+
+## Installation
+
+[Download a release binary](https://github.com/pancsta/asyncmachine-go/releases/latest) or use `go install`:
+
+`go install github.com/pancsta/asyncmachine-go/tools/am-dbg@latest`
+
+## Example Data
+
+For a quick demo you can browse an exported dump located in `assets/am-dbg-sim.gob.bz2` by running:
+
+`$ am-dbg -i assets/am-dbg-sim.gob.bz2 -m sim-p1 -t 25`
+
+## Steps To Debug
+
+1. Set up telemetry:
+
+ ```go
+ import "github.com/pancsta/asyncmachine-go/pkg/telemetry"
+ // ...
+ err := telemetry.MonitorTransitions(mach, telemetry.RpcHost)
+ ```
+
+2. Run `am-dbg`
+3. Run your code
+4. Your machine should show up in the debugger
+
+## FAQ
+
+### How to debug steps of a transition
+
+Go to the steps timelines (bottom one) using the Tab key, then press left/right like before.
+
+### How to export data
+
+Press `alt+s` and Enter.
+
+### How to access the help screen
+
+Press `?` to show the help popup.
diff --git a/tools/cmd/am-dbg/main.go b/tools/cmd/am-dbg/main.go
new file mode 100644
index 0000000..029cb3d
--- /dev/null
+++ b/tools/cmd/am-dbg/main.go
@@ -0,0 +1,229 @@
+package main
+
+import (
+ "context"
+ "encoding/gob"
+ "fmt"
+ "log"
+ "os"
+ "runtime/debug"
+ "time"
+
+ am "github.com/pancsta/asyncmachine-go/pkg/machine"
+ "github.com/pancsta/asyncmachine-go/pkg/telemetry"
+ "github.com/pancsta/asyncmachine-go/tools/debugger"
+ ss "github.com/pancsta/asyncmachine-go/tools/debugger/states"
+
+ "github.com/spf13/cobra"
+)
+
+const (
+ cliParamLogFile = "log-file"
+ cliParamLogLevel = "log-level"
+ cliParamServerURL = "listen-on"
+ cliParamServerURLShort = "l"
+ cliParamAmDbgURL = "am-dbg-url"
+ cliParamEnableMouse = "enable-mouse"
+ cliParamCleanOnConnect = "clean-on-connect"
+ cliParamVersion = "version"
+ cliParamImport = "import-data"
+ cliParamImportShort = "i"
+ cliParamSelectConn = "select-connected"
+ cliParamSelectConnShort = "c"
+ cliParamStartupMach = "select-machine"
+ cliParamStartupMachShort = "m"
+ cliParamStartupTx = "select-transition"
+ cliParamStartupTxShort = "t"
+ cliParamView = "view"
+ cliParamViewShort = "v"
+)
+
+func main() {
+ rootCmd := getRootCmd()
+ err := rootCmd.Execute()
+ if err != nil {
+ panic(err)
+ }
+}
+
+func getRootCmd() *cobra.Command {
+ rootCmd := &cobra.Command{
+ Use: "am-dbg",
+ Run: cliRun,
+ }
+
+ // TODO --dark-mode auto|on|off
+ // TODO --rename-duplicates renames disconnected dups as TIME-NAME
+ rootCmd.Flags().String(cliParamLogFile, "am-dbg.log",
+ "Log file path")
+ rootCmd.Flags().Int(cliParamLogLevel, 0,
+ "Log level, 0-5 (silent-everything)")
+ rootCmd.Flags().StringP(cliParamServerURL,
+ cliParamServerURLShort, telemetry.DbgHost,
+ "Host and port for the debugger to listen on")
+ rootCmd.Flags().String(cliParamAmDbgURL, "",
+ "Debug this instance of am-dbg with another one")
+ rootCmd.Flags().StringP(cliParamView, cliParamViewShort, "tree-log",
+ "Initial view (tree-log, tree-matrix, matrix)")
+ rootCmd.Flags().StringP(cliParamStartupMach,
+ cliParamStartupMachShort, "",
+ "Select a machine by ID on startup (requires --"+cliParamImport+")")
+ // TODO parse copy-paste commas, eg 1,001
+ rootCmd.Flags().IntP(cliParamStartupTx, cliParamStartupTxShort, 0,
+ "Select a transaction by _number_ on startup (requires --"+
+ cliParamStartupMach+")")
+ rootCmd.Flags().Bool(cliParamEnableMouse, false,
+ "Enable mouse support (experimental)")
+ rootCmd.Flags().Bool(cliParamCleanOnConnect, false,
+ "Clean up disconnected clients on the 1st connection")
+ rootCmd.Flags().BoolP(cliParamSelectConn, cliParamSelectConnShort, false,
+ "Select the newly connected machine, if no other is connected")
+ rootCmd.Flags().StringP(cliParamImport, cliParamImportShort, "",
+ "Import an exported gob.bz2 file")
+ rootCmd.Flags().Bool(cliParamVersion, false,
+ "Print version and exit")
+
+ return rootCmd
+}
+
+// TODO error msgs
+func cliRun(cmd *cobra.Command, _ []string) {
+
+ // params
+ version, err := cmd.Flags().GetBool(cliParamVersion)
+ if err != nil {
+ panic(err)
+ }
+
+ logFile := cmd.Flag(cliParamLogFile).Value.String()
+ logLevelInt, err := cmd.Flags().GetInt(cliParamLogLevel)
+ if err != nil {
+ panic(err)
+ }
+
+ logLevel := am.LogLevel(logLevelInt)
+ serverURL := cmd.Flag(cliParamServerURL).Value.String()
+ debugURL := cmd.Flag(cliParamAmDbgURL).Value.String()
+ importGob := cmd.Flag(cliParamImport).Value.String()
+ startupMachine := cmd.Flag(cliParamStartupMach).Value.String()
+ startupView := cmd.Flag(cliParamView).Value.String()
+ startupTx, err := cmd.Flags().GetInt(cliParamStartupTx)
+ if err != nil {
+ panic(err)
+ }
+
+ enableMouse, err := cmd.Flags().GetBool(cliParamEnableMouse)
+ if err != nil {
+ panic(err)
+ }
+
+ cleanOnConnect, err := cmd.Flags().GetBool(cliParamCleanOnConnect)
+ if err != nil {
+ panic(err)
+ }
+
+ selectConnected, err := cmd.Flags().GetBool(cliParamSelectConn)
+ if err != nil {
+ panic(err)
+ }
+
+ if version {
+ build, ok := debug.ReadBuildInfo()
+ if !ok {
+ panic("No build info available")
+ }
+ fmt.Println(build.Main.Version)
+ os.Exit(0)
+ }
+
+ // file logging
+ if logFile != "" {
+ _ = os.Remove(logFile)
+ file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY, 0o666)
+ if err != nil {
+ panic(err)
+ }
+ defer file.Close()
+ log.SetOutput(file)
+ }
+
+ // init the debugger
+ // TODO extract to tools/debugger
+ dbg := &debugger.Debugger{
+ URL: serverURL,
+ EnableMouse: enableMouse,
+ CleanOnConnect: cleanOnConnect,
+ SelectConnected: selectConnected,
+ Clients: make(map[string]*debugger.Client),
+ }
+ gob.Register(debugger.Exportable{})
+ gob.Register(am.Relation(0))
+
+ // import data
+ if importGob != "" {
+ dbg.ImportData(importGob)
+ }
+
+ // init machine
+ dbg.Mach, err = initMachine(dbg, logLevel)
+ if err != nil {
+ panic(err)
+ }
+
+ // rpc client
+ if debugURL != "" {
+ err := telemetry.TransitionsToDBG(dbg.Mach, debugURL)
+ // TODO retries
+ if err != nil {
+ panic(err)
+ }
+ }
+
+ // rpc server
+ go debugger.StartRCP(dbg.Mach, serverURL)
+
+ // start and wait till the end
+ dbg.Mach.Add1(ss.Start, am.A{
+ "Client.id": startupMachine,
+ "Client.cursorTx": startupTx,
+ "dbgView": startupView,
+ })
+ <-dbg.Mach.WhenNot1(ss.Start, nil)
+ txes := 0
+ for _, c := range dbg.Clients {
+ txes += len(c.MsgTxs)
+ }
+
+ // TODO format numbers
+ _, _ = dbg.P.Printf("Clients: %d\n", len(dbg.Clients))
+ _, _ = dbg.P.Printf("Transitions: %d\n", txes)
+}
+
+// TODO extract to tools/debugger
+func initMachine(
+ h *debugger.Debugger, logLevel am.LogLevel) (*am.Machine, error) {
+ ctx := context.Background()
+
+ // TODO use NewCommon
+ mach := am.New(ctx, ss.States, &am.Opts{
+ HandlerTimeout: time.Hour,
+ DontPanicToException: true,
+ DontLogID: true,
+ })
+ mach.ID = "am-dbg-" + mach.ID
+
+ err := mach.VerifyStates(ss.Names)
+ if err != nil {
+ return nil, err
+ }
+
+ mach.SetTestLogger(log.Printf, logLevel)
+ mach.SetLogArgs(am.NewArgsMapper([]string{"Machine.id", "conn_id"}, 20))
+
+ err = mach.BindHandlers(h)
+ if err != nil {
+ return nil, err
+ }
+
+ return mach, nil
+}
diff --git a/tools/cmd/am-gen/README.md b/tools/cmd/am-gen/README.md
new file mode 100644
index 0000000..dd56ea9
--- /dev/null
+++ b/tools/cmd/am-gen/README.md
@@ -0,0 +1,54 @@
+# am-gen
+
+`am-gen` will quickly bootstrap a typesafe states file for you.
+
+`$ am-gen states-file Foo,Bar`
+
+
+
+Example template for Foo and Bar
+
+```go
+package states
+
+import am "github.com/pancsta/asyncmachine-go/pkg/machine"
+
+// S is a type alias for a list of state names.
+type S = am.S
+
+// States map defines relations and properties of states.
+var States = am.Struct{
+ Foo: {},
+ Bar: {},
+}
+
+// Groups of mutually exclusive states.
+
+//var (
+// GroupPlaying = S{Playing, Paused}
+//)
+
+//#region boilerplate defs
+
+// Names of all the states (pkg enum).
+
+const (
+ Foo = "Foo"
+ Bar = "Bar"
+)
+
+// Names is an ordered list of all the state names.
+var Names = S{
+ Foo,
+ Bar,
+ am.Exception,
+}
+
+//#endregion
+```
+
+
+
+## Installation
+
+`go install github.com/pancsta/asyncmachine-go/tools/am-gen@latest`
diff --git a/tools/cmd/am-gen/main.go b/tools/cmd/am-gen/main.go
new file mode 100644
index 0000000..c90ce9b
--- /dev/null
+++ b/tools/cmd/am-gen/main.go
@@ -0,0 +1,27 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/pancsta/asyncmachine-go/tools/generator"
+)
+
+func main() {
+ usage := "Usage: am-gen states-file State1,State2"
+
+ // require 1st and 2nd args
+ if len(os.Args) < 3 {
+ fmt.Println(usage)
+ os.Exit(1)
+ }
+
+ states := strings.Split(os.Args[2], ",")
+ if len(states) == 0 {
+ fmt.Println(usage)
+ os.Exit(1)
+ }
+
+ fmt.Print(generator.GenStatesFile(states))
+}
diff --git a/tools/debugger/debugger.go b/tools/debugger/debugger.go
new file mode 100644
index 0000000..8e25823
--- /dev/null
+++ b/tools/debugger/debugger.go
@@ -0,0 +1,1896 @@
+// Package debugger provides a TUI debugger with multi-client support. Runnable
+// command can be found in tools/cmd/am-dbg.
+package debugger
+
+import (
+ "encoding/gob"
+ "fmt"
+ "log"
+ "os"
+ "path"
+ "regexp"
+ "slices"
+ "sort"
+ "strconv"
+ "strings"
+ "time"
+
+ "code.rocketnine.space/tslocum/cbind"
+ "github.com/dsnet/compress/bzip2"
+ "github.com/gdamore/tcell/v2"
+ "github.com/lithammer/dedent"
+ "github.com/pancsta/cview"
+ "github.com/samber/lo"
+ "golang.org/x/exp/maps"
+ "golang.org/x/text/message"
+
+ am "github.com/pancsta/asyncmachine-go/pkg/machine"
+ "github.com/pancsta/asyncmachine-go/pkg/telemetry"
+ ss "github.com/pancsta/asyncmachine-go/tools/debugger/states"
+)
+
+const (
+ // TODO light mode
+ colorActive = tcell.ColorOlive
+ colorInactive = tcell.ColorLimeGreen
+ colorHighlight = tcell.ColorDarkSlateGray
+ colorHighlight2 = tcell.ColorDimGray
+ // TODO customize
+ playInterval = 500 * time.Millisecond
+ // TODO add param --max-clients
+ maxClients = 150
+ timeFormat = "15:04:05.000000000"
+ fastJumpAmount = 50
+ arrowThrottleMs = 200
+ logUpdateDebounce = 300 * time.Millisecond
+ sidebarUpdateDebounce = time.Second
+ searchAsTypeWindow = 1500 * time.Millisecond
+)
+
+type Debugger struct {
+ am.ExceptionHandler
+ Mach *am.Machine
+ Clients map[string]*Client
+ EnableMouse bool
+ CleanOnConnect bool
+ SelectConnected bool
+
+ // current client
+ C *Client
+ app *cview.Application
+ tree *cview.TreeView
+ treeRoot *cview.TreeNode
+ log *cview.TextView
+ timelineTxs *cview.ProgressBar
+ timelineSteps *cview.ProgressBar
+ // TODO take from views
+ focusable []*cview.Box
+ playTimer *time.Ticker
+ currTxBarRight *cview.TextView
+ currTxBarLeft *cview.TextView
+ nextTxBarLeft *cview.TextView
+ nextTxBarRight *cview.TextView
+ helpDialog *cview.Flex
+ keystrokesBar *cview.TextView
+ layoutRoot *cview.Panels
+ sidebar *cview.List
+ mainGrid *cview.Grid
+ URL string
+ logRebuildEnd int
+ prevClientTxTime time.Time
+ repaintScheduled bool
+ updateSidebarScheduled bool
+ lastKey tcell.Key
+ lastKeyTime time.Time
+ P *message.Printer
+ updateLogScheduled bool
+ matrix *cview.Table
+ focusManager *cview.FocusManager
+ exportDialog *cview.Modal
+ contentPanels *cview.Panels
+}
+
+type Exportable struct {
+ MsgStruct *telemetry.DbgMsgStruct
+ MsgTxs []*telemetry.DbgMsgTx
+}
+
+type Client struct {
+ // bits which get saved into the go file
+ Exportable
+ // current transition, 1-based, mirrors the slider
+ CursorTx int
+ // current step, 1-based, mirrors the slider
+ CursorStep int
+
+ id string
+ connID string
+ // TODO extract from msgs
+ lastActive time.Time
+ connected bool
+ selectedState string
+ // processed
+ msgTxsParsed []MsgTxParsed
+ // processed
+ logMsgs []string
+}
+
+type MsgTxParsed struct {
+ StatesAdded am.S
+ StatesRemoved am.S
+ StatesTouched am.S
+}
+
+type nodeRef struct {
+ // TODO type
+ // type nodeType
+ // node is a state (reference or top level)
+ stateName string
+ // node is a state reference, not a top level state
+ // eg Bar in case of: Foo -> Remove -> Bar
+ // TODO name collision with nodeRef
+ isRef bool
+ // node is a relation (Remove, Add, Require, After)
+ isRel bool
+ // relation type (if isRel)
+ rel am.Relation
+ // top level state name (for both rels and refs)
+ parentState string
+ // node touched by a transition step
+ touched bool
+ // expanded by the user
+ expanded bool
+ // node is a state property (Auto, Multi)
+ isProp bool
+ propLabel string
+}
+
+// ScrollToStateTx scrolls to the next transition involving the state being
+// activated or deactivated. If fwd is true, it scrolls forward, otherwise
+// backwards.
+func (d *Debugger) ScrollToStateTx(state string, fwd bool) {
+ c := d.C
+ if c == nil {
+ return
+ }
+ step := -1
+ if fwd {
+ step = 1
+ }
+
+ for i := c.CursorTx + step; i > 0 && i < len(c.MsgTxs)+1; i = i + step {
+
+ parsed := c.msgTxsParsed[i-1]
+ if !slices.Contains(parsed.StatesAdded, state) &&
+ !slices.Contains(parsed.StatesRemoved, state) {
+ continue
+ }
+
+ // scroll to this tx
+ d.Mach.Add1(ss.ScrollToTx, am.A{"Client.cursorTx": i})
+ d.Mach.Remove1(ss.TailMode, nil)
+ break
+ }
+}
+
+// RedrawFull updates all components of the debugger UI, except the sidebar.
+func (d *Debugger) RedrawFull(immediate bool) {
+ d.updateViews(immediate)
+ d.updateTimelines()
+ d.updateTxBars()
+ d.updateKeyBars()
+ d.updateBorderColor()
+ d.draw()
+}
+
+func (d *Debugger) NextTx() *telemetry.DbgMsgTx {
+ c := d.C
+ if c == nil {
+ return nil
+ }
+ onLastTx := c.CursorTx >= len(c.MsgTxs)
+ if onLastTx {
+ return nil
+ }
+ return c.MsgTxs[c.CursorTx]
+}
+
+func (d *Debugger) CurrentTx() *telemetry.DbgMsgTx {
+ c := d.C
+ if c == nil {
+ return nil
+ }
+ if c.CursorTx == 0 || len(c.MsgTxs) < c.CursorTx {
+ return nil
+ }
+ return c.MsgTxs[c.CursorTx-1]
+}
+
+func (d *Debugger) PrevTx() *telemetry.DbgMsgTx {
+ c := d.C
+ if c == nil {
+ return nil
+ }
+ if c.CursorTx < 2 {
+ return nil
+ }
+ return c.MsgTxs[c.CursorTx-2]
+}
+
+func (d *Debugger) initUIComponents() {
+ d.helpDialog = d.initHelpDialog()
+ d.exportDialog = d.initExportDialog()
+
+ // tree view
+ d.tree = d.initMachineTree()
+ d.tree.SetTitle(" Structure ")
+ d.tree.SetBorder(true)
+
+ // sidebar
+ d.sidebar = cview.NewList()
+ d.sidebar.SetTitle(" Machines ")
+ d.sidebar.SetBorder(true)
+ d.sidebar.ShowSecondaryText(false)
+ d.sidebar.SetSelectedFocusOnly(true)
+ d.sidebar.SetMainTextColor(colorActive)
+ d.sidebar.SetSelectedTextColor(tcell.ColorWhite)
+ d.sidebar.SetSelectedBackgroundColor(colorHighlight2)
+ d.sidebar.SetHighlightFullLine(true)
+ d.sidebar.SetSelectedFunc(func(i int, listItem *cview.ListItem) {
+ cid := listItem.GetReference().(string)
+ d.Mach.Add1(ss.SelectingClient, am.A{"Client.id": cid})
+ })
+ d.sidebar.SetSelectedAlwaysVisible(true)
+
+ // log view
+ d.log = cview.NewTextView()
+ d.log.SetBorder(true)
+ d.log.SetRegions(true)
+ d.log.SetTextAlign(cview.AlignLeft)
+ d.log.SetWrap(true)
+ d.log.SetDynamicColors(true)
+ d.log.SetTitle(" Log ")
+ d.log.SetHighlightForegroundColor(tcell.ColorWhite)
+ d.log.SetHighlightBackgroundColor(colorHighlight2)
+
+ // matrix
+ d.matrix = cview.NewTable()
+ d.matrix.SetBorder(true)
+ d.matrix.SetTitle(" Matrix ")
+ // TODO draw scrollbar at the right edge
+ d.matrix.SetScrollBarVisibility(cview.ScrollBarNever)
+ d.matrix.SetPadding(0, 0, 1, 0)
+
+ // current tx bar
+ d.currTxBarLeft = cview.NewTextView()
+ d.currTxBarLeft.SetDynamicColors(true)
+ d.currTxBarRight = cview.NewTextView()
+ d.currTxBarRight.SetTextAlign(cview.AlignRight)
+
+ // next tx bar
+ d.nextTxBarLeft = cview.NewTextView()
+ d.nextTxBarLeft.SetDynamicColors(true)
+ d.nextTxBarRight = cview.NewTextView()
+ d.nextTxBarRight.SetTextAlign(cview.AlignRight)
+
+ // TODO step info bar: type, from, to, data
+ // timeline tx
+ d.timelineTxs = cview.NewProgressBar()
+ d.timelineTxs.SetBorder(true)
+
+ // timeline steps
+ d.timelineSteps = cview.NewProgressBar()
+ d.timelineSteps.SetBorder(true)
+
+ // keystrokes bar
+ d.keystrokesBar = cview.NewTextView()
+ d.keystrokesBar.SetTextAlign(cview.AlignCenter)
+ d.keystrokesBar.SetDynamicColors(true)
+
+ // update models
+ d.updateTimelines()
+ d.updateTxBars()
+ d.updateKeyBars()
+ d.updateFocusable()
+}
+
+func (d *Debugger) initLayout() {
+ // TODO flexbox
+ currTxBar := cview.NewGrid()
+ currTxBar.AddItem(d.currTxBarLeft, 0, 0, 1, 1, 0, 0, false)
+ currTxBar.AddItem(d.currTxBarRight, 0, 1, 1, 1, 0, 0, false)
+
+ // TODO flexbox
+ nextTxBar := cview.NewGrid()
+ nextTxBar.AddItem(d.nextTxBarLeft, 0, 0, 1, 1, 0, 0, false)
+ nextTxBar.AddItem(d.nextTxBarRight, 0, 1, 1, 1, 0, 0, false)
+
+ // content grid
+ treeLogGrid := cview.NewGrid()
+ treeLogGrid.SetRows(-1)
+ treeLogGrid.SetColumns( /*tree*/ -1 /*log*/, -1, -1)
+ treeLogGrid.AddItem(d.tree, 0, 0, 1, 1, 0, 0, false)
+ treeLogGrid.AddItem(d.log, 0, 1, 1, 2, 0, 0, false)
+
+ treeMatrixGrid := cview.NewGrid()
+ treeMatrixGrid.SetRows(-1)
+ treeMatrixGrid.SetColumns( /*tree*/ -1 /*log*/, -1, -1)
+ treeMatrixGrid.AddItem(d.tree, 0, 0, 1, 1, 0, 0, false)
+ treeMatrixGrid.AddItem(d.matrix, 0, 1, 1, 2, 0, 0, false)
+
+ // content panels
+ d.contentPanels = cview.NewPanels()
+ d.contentPanels.AddPanel("tree-log", treeLogGrid, true, true)
+ d.contentPanels.AddPanel("tree-matrix", treeMatrixGrid, true, false)
+ d.contentPanels.AddPanel("matrix", d.matrix, true, false)
+ d.contentPanels.SetBackgroundColor(colorHighlight)
+
+ // main grid
+ mainGrid := cview.NewGrid()
+ mainGrid.SetRows(-1, 2, 3, 2, 3, 2)
+ cols := []int{ /*sidebar*/ -1 /*content*/, -1, -1, -1, -1, -1, -1}
+ mainGrid.SetColumns(cols...)
+ // row 1 left
+ mainGrid.AddItem(d.sidebar, 0, 0, 1, 1, 0, 0, false)
+ // row 1 mid, right
+ mainGrid.AddItem(d.contentPanels, 0, 1, 1, 6, 0, 0, false)
+ // row 2...5
+ mainGrid.AddItem(currTxBar, 1, 0, 1, len(cols), 0, 0, false)
+ mainGrid.AddItem(d.timelineTxs, 2, 0, 1, len(cols), 0, 0, false)
+ mainGrid.AddItem(nextTxBar, 3, 0, 1, len(cols), 0, 0, false)
+ mainGrid.AddItem(d.timelineSteps, 4, 0, 1, len(cols), 0, 0, false)
+ mainGrid.AddItem(d.keystrokesBar, 5, 0, 1, len(cols), 0, 0, false)
+
+ panels := cview.NewPanels()
+ panels.AddPanel("export", d.exportDialog, false, true)
+ panels.AddPanel("help", d.helpDialog, true, true)
+ panels.AddPanel("main", mainGrid, true, true)
+
+ d.mainGrid = mainGrid
+ d.layoutRoot = panels
+}
+
+type Focusable struct {
+ cview.Primitive
+ *cview.Box
+}
+
+// TODO feat: support dialogs
+// TODO refac: generate d.focusable list via GetFocusable
+// TODO fix: preserve currently focused element
+func (d *Debugger) updateFocusable() {
+ if d.focusManager == nil {
+ d.Mach.Log("Error: focus manager not initialized")
+ return
+ }
+
+ var prims []cview.Primitive
+ switch d.Mach.Switch(ss.GroupViews...) {
+
+ case ss.MatrixView:
+ d.focusable = []*cview.Box{
+ d.sidebar.Box, d.matrix.Box, d.timelineTxs.Box, d.timelineSteps.Box,
+ }
+ prims = []cview.Primitive{
+ d.sidebar, d.matrix, d.timelineTxs,
+ d.timelineSteps,
+ }
+
+ case ss.TreeMatrixView:
+ d.focusable = []*cview.Box{
+ d.sidebar.Box, d.tree.Box, d.matrix.Box, d.timelineTxs.Box,
+ d.timelineSteps.Box,
+ }
+ prims = []cview.Primitive{
+ d.sidebar, d.tree, d.matrix, d.timelineTxs,
+ d.timelineSteps,
+ }
+
+ case ss.TreeLogView:
+ fallthrough
+ default:
+ d.focusable = []*cview.Box{
+ d.sidebar.Box, d.tree.Box, d.log.Box, d.timelineTxs.Box,
+ d.timelineSteps.Box,
+ }
+ prims = []cview.Primitive{
+ d.sidebar, d.tree, d.log, d.timelineTxs,
+ d.timelineSteps,
+ }
+
+ }
+
+ d.focusManager.Reset()
+ d.focusManager.Add(prims...)
+
+ switch d.Mach.Switch(ss.GroupFocused...) {
+ case ss.SidebarFocused:
+ d.focusManager.Focus(d.sidebar)
+ case ss.TreeFocused:
+ if d.Mach.Any1(ss.TreeMatrixView, ss.TreeLogView) {
+ d.focusManager.Focus(d.tree)
+ } else {
+ d.focusManager.Focus(d.sidebar)
+ }
+ case ss.LogFocused:
+ if d.Mach.Is1(ss.TreeLogView) {
+ d.focusManager.Focus(d.tree)
+ } else {
+ d.focusManager.Focus(d.sidebar)
+ }
+ d.focusManager.Focus(d.log)
+ case ss.MatrixFocused:
+ if d.Mach.Any1(ss.TreeMatrixView, ss.MatrixView) {
+ d.focusManager.Focus(d.matrix)
+ } else {
+ d.focusManager.Focus(d.sidebar)
+ }
+ case ss.TimelineTxsFocused:
+ d.focusManager.Focus(d.timelineTxs)
+ case ss.TimelineStepsFocused:
+ d.focusManager.Focus(d.timelineSteps)
+ default:
+ d.focusManager.Focus(d.sidebar)
+ }
+}
+
+// TODO tab navigation
+// TODO delegated flow machine?
+func (d *Debugger) initExportDialog() *cview.Modal {
+ exportDialog := cview.NewModal()
+ form := exportDialog.GetForm()
+ form.AddInputField("Filename", "am-dbg-dump", 20, nil, nil)
+
+ exportDialog.SetText("Export all clients data (esc quits)")
+ exportDialog.AddButtons([]string{"Save"})
+ // TODO support cancel
+ // exportDialog.AddButtons([]string{"Save", "Cancel"})
+ exportDialog.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
+ field, ok := form.GetFormItemByLabel("Filename").(*cview.InputField)
+ if !ok {
+ d.Mach.Log("Error: export dialog field not found")
+ return
+ }
+ filename := field.GetText()
+
+ if buttonLabel == "Save" && filename != "" {
+ form.GetButton(0).SetLabel("Saving...")
+ form.Draw(d.app.GetScreen())
+ d.exportData(filename)
+ d.Mach.Remove1(ss.ExportDialog, nil)
+ form.GetButton(0).SetLabel("Save")
+ } else if buttonLabel == "Cancel" {
+ d.Mach.Remove1(ss.ExportDialog, nil)
+ }
+ })
+
+ return exportDialog
+}
+
+// TODO better approach to modals
+// TODO modal titles
+// TODO state color meanings
+// TODO page up/down on tx timeline
+func (d *Debugger) initHelpDialog() *cview.Flex {
+ left := cview.NewTextView()
+ left.SetBackgroundColor(colorHighlight)
+ left.SetTitle(" Legend ")
+ left.SetDynamicColors(true)
+ left.SetPadding(1, 1, 1, 1)
+ left.SetText(dedent.Dedent(strings.Trim(`
+ [::b]### [::u]tree legend[::-]
+
+ [::b]*[::-] handler ran
+ [::b]+[::-] to be added
+ [::b]-[::-] to be removed
+ [::b]bold[::-] touched state
+ [::b]underline[::-] called state
+ [::b]![::-] state canceled
+
+ [::b]### [::u]matrix legend[::-]
+
+ [::b]underline[::-] called state
+
+ [::b]1st row[::-] called states
+ col == state index
+
+ [::b]2nd row[::-] state tick changes
+ col == state index
+
+ [::b]>=3 row[::-] state relations
+ cartesian product
+ col == source state index
+ row == target state index
+
+ `, "\n ")))
+
+ right := cview.NewTextView()
+ right.SetBackgroundColor(colorHighlight)
+ right.SetTitle(" Keystrokes ")
+ right.SetDynamicColors(true)
+ right.SetPadding(1, 1, 1, 1)
+ right.SetText(dedent.Dedent(strings.Trim(`
+ [::b]### [::u]keystrokes[::-]
+
+ [::b]tab[::-] change focus
+ [::b]shift+tab[::-] change focus
+ [::b]space[::-] play/pause
+ [::b]left/right[::-] back/fwd
+ [::b]alt+left/right[::-] fast jump
+ [::b]alt+h/l[::-] fast jump
+ [::b]alt+h/l[::-] state jump (if selected)
+ [::b]up/down[::-] scroll / navigate
+ [::b]j/k[::-] scroll / navigate
+ [::b]alt+j/k[::-] page up/down
+ [::b]alt+e[::-] expand/collapse tree
+ [::b]enter[::-] expand/collapse node
+ [::b]alt+v[::-] tail mode
+ [::b]alt+m[::-] matrix view
+ [::b]home/end[::-] struct / last tx
+ [::b]alt+s[::-] export data
+ [::b]backspace[::-] remove machine
+ [::b]ctrl+q[::-] quit
+ [::b]?[::-] show help
+ `, "\n ")))
+
+ grid := cview.NewGrid()
+ grid.SetTitle(" AsyncMachine Debugger ")
+ grid.SetColumns(0, 0)
+ grid.SetRows(0)
+ grid.AddItem(left, 0, 0, 1, 1, 0, 0, false)
+ grid.AddItem(right, 0, 1, 1, 1, 0, 0, false)
+
+ box1 := cview.NewBox()
+ box1.SetBackgroundTransparent(true)
+ box2 := cview.NewBox()
+ box2.SetBackgroundTransparent(true)
+ box3 := cview.NewBox()
+ box3.SetBackgroundTransparent(true)
+ box4 := cview.NewBox()
+ box4.SetBackgroundTransparent(true)
+
+ flexHor := cview.NewFlex()
+ flexHor.AddItem(box1, 0, 1, false)
+ flexHor.AddItem(grid, 0, 2, false)
+ flexHor.AddItem(box2, 0, 1, false)
+
+ flexVer := cview.NewFlex()
+ flexVer.SetDirection(cview.FlexRow)
+ flexVer.AddItem(box3, 0, 1, false)
+ flexVer.AddItem(flexHor, 0, 2, false)
+ flexVer.AddItem(box4, 0, 1, false)
+
+ return flexVer
+}
+
+func (d *Debugger) updateViews(immediate bool) {
+ switch d.Mach.Switch(ss.GroupViews...) {
+
+ case ss.MatrixView:
+ d.updateMatrix()
+ d.contentPanels.HidePanel("tree-log")
+ d.contentPanels.HidePanel("tree-matrix")
+
+ d.contentPanels.ShowPanel("matrix")
+
+ case ss.TreeMatrixView:
+ d.updateMatrix()
+ d.updateTree()
+ d.contentPanels.HidePanel("matrix")
+ d.contentPanels.HidePanel("tree-log")
+
+ d.contentPanels.ShowPanel("tree-matrix")
+
+ case ss.TreeLogView:
+ fallthrough
+ default:
+ d.updateTree()
+ d.updateLog(immediate)
+ d.contentPanels.HidePanel("matrix")
+ d.contentPanels.HidePanel("tree-matrix")
+
+ d.contentPanels.ShowPanel("tree-log")
+ }
+}
+
+func (d *Debugger) bindKeyboard() {
+ // focus manager
+ d.focusManager = cview.NewFocusManager(d.app.SetFocus)
+ d.focusManager.SetWrapAround(true)
+ inputHandler := cbind.NewConfiguration()
+ d.app.SetAfterFocusFunc(d.afterFocus())
+
+ focusChange := func(f func()) func(ev *tcell.EventKey) *tcell.EventKey {
+ return func(ev *tcell.EventKey) *tcell.EventKey {
+ // keep Tab inside dialogs
+ if d.Mach.Any1(ss.GroupDialog...) {
+ return ev
+ }
+
+ // fwd to FocusManager
+ f()
+ return nil
+ }
+ }
+
+ // tab
+ for _, key := range cview.Keys.MovePreviousField {
+ err := inputHandler.Set(key, focusChange(d.focusManager.FocusPrevious))
+ if err != nil {
+ log.Printf("Error: binding keys %s", err)
+ }
+ }
+
+ // shift+tab
+ for _, key := range cview.Keys.MoveNextField {
+ err := inputHandler.Set(key, focusChange(d.focusManager.FocusNext))
+ if err != nil {
+ log.Printf("Error: binding keys %s", err)
+ }
+ }
+
+ // custom keys
+ for key, fn := range d.getKeystrokes() {
+ err := inputHandler.Set(key, fn)
+ if err != nil {
+ log.Printf("Error: binding keys %s", err)
+ }
+ }
+
+ d.searchTreeSidebar(inputHandler)
+ d.app.SetInputCapture(inputHandler.Capture)
+}
+
+// afterFocus forwards focus events to machine states
+func (d *Debugger) afterFocus() func(p cview.Primitive) {
+ return func(p cview.Primitive) {
+ switch p {
+
+ case d.tree:
+ fallthrough
+ case d.tree.Box:
+ d.Mach.Add1(ss.TreeFocused, nil)
+
+ case d.log:
+ fallthrough
+ case d.log.Box:
+ d.Mach.Add1(ss.LogFocused, nil)
+
+ case d.timelineTxs:
+ fallthrough
+ case d.timelineTxs.Box:
+ d.Mach.Add1(ss.TimelineTxsFocused, nil)
+
+ case d.timelineSteps:
+ fallthrough
+ case d.timelineSteps.Box:
+ d.Mach.Add1(ss.TimelineStepsFocused, nil)
+
+ case d.sidebar:
+ fallthrough
+ case d.sidebar.Box:
+ d.Mach.Add1(ss.SidebarFocused, nil)
+
+ case d.matrix:
+ fallthrough
+ case d.matrix.Box:
+ d.Mach.Add1(ss.MatrixFocused, nil)
+
+ case d.helpDialog:
+ fallthrough
+ case d.helpDialog.Box:
+ fallthrough
+ case d.exportDialog:
+ fallthrough
+ case d.exportDialog.Box:
+ d.Mach.Add1(ss.DialogFocused, nil)
+ }
+
+ // update the log highlight on focus change
+ if d.Mach.Is1(ss.TreeLogView) {
+ d.updateLog(true)
+ }
+
+ d.updateSidebar(true)
+ }
+}
+
+// searchTreeSidebar searches for a-z, -, _ in the tree and sidebar, with a
+// searchAsTypeWindow buffer.
+func (d *Debugger) searchTreeSidebar(inputHandler *cbind.Configuration) {
+ var (
+ bufferStart time.Time
+ buffer string
+ keys = []string{"-", "_"}
+ )
+
+ for i := 0; i < 26; i++ {
+ keys = append(keys,
+ fmt.Sprintf("%c", 'a'+i))
+ }
+
+ for _, key := range keys {
+ key := key
+ err := inputHandler.Set(key, func(ev *tcell.EventKey) *tcell.EventKey {
+ if d.Mach.Not(am.S{ss.SidebarFocused, ss.TreeFocused}) {
+ return ev
+ }
+
+ // buffer
+ if bufferStart.Add(searchAsTypeWindow).After(time.Now()) {
+ buffer += key
+ } else {
+ bufferStart = time.Now()
+ buffer = key
+ }
+
+ // find the first client starting with the key
+
+ // sidebar
+ if d.Mach.Is1(ss.SidebarFocused) {
+ for i, item := range d.sidebar.GetItems() {
+ text := normalizeText(item.GetMainText())
+ if strings.HasPrefix(text, buffer) {
+ d.sidebar.SetCurrentItem(i)
+ d.updateSidebar(true)
+
+ d.draw()
+ break
+ }
+ }
+ } else if d.Mach.Is1(ss.TreeFocused) {
+
+ // tree
+ found := false
+ d.treeRoot.WalkUnsafe(
+ func(node, parent *cview.TreeNode, depth int) bool {
+ if found {
+ return false
+ }
+
+ text := normalizeText(node.GetText())
+
+ if parent != nil && parent.IsExpanded() &&
+ strings.HasPrefix(text, buffer) {
+ d.Mach.Remove1(ss.StateNameSelected, nil)
+ d.tree.SetCurrentNode(node)
+ d.updateTree()
+ d.draw()
+ found = true
+
+ return false
+ }
+
+ return true
+ })
+ }
+
+ return nil
+ })
+ if err != nil {
+ log.Printf("Error: binding keys %s", err)
+ }
+ }
+}
+
+// regexp removing [foo]
+var re = regexp.MustCompile(`\[(.*?)\]`)
+
+func normalizeText(text string) string {
+ return strings.ToLower(re.ReplaceAllString(text, ""))
+}
+
+func (d *Debugger) getKeystrokes() map[string]func(
+ ev *tcell.EventKey) *tcell.EventKey {
+ // TODO add state deps to the keystrokes structure
+ // TODO use tcell.KeyNames instead of strings as keys
+ // TODO rate limit
+ return map[string]func(ev *tcell.EventKey) *tcell.EventKey{
+ // play/pause
+ "space": func(ev *tcell.EventKey) *tcell.EventKey {
+ if d.Mach.Not1(ss.ClientSelected) {
+ return nil
+ }
+
+ if d.Mach.Is1(ss.Paused) {
+ d.Mach.Add1(ss.Playing, nil)
+ } else {
+ d.Mach.Add1(ss.Paused, nil)
+ }
+
+ return nil
+ },
+
+ // prev tx
+ "left": func(ev *tcell.EventKey) *tcell.EventKey {
+ if d.Mach.Not1(ss.ClientSelected) {
+ return nil
+ }
+ if d.throttleKey(ev, arrowThrottleMs) {
+ // TODO fast jump scroll while holding the key
+ return nil
+ }
+
+ // scroll matrix
+ if d.Mach.Is1(ss.MatrixFocused) {
+ return ev
+ }
+
+ // scroll timelines
+ if d.Mach.Is1(ss.TimelineStepsFocused) {
+ d.Mach.Add1(ss.UserBackStep, nil)
+ } else {
+ d.Mach.Add1(ss.UserBack, nil)
+ }
+
+ return nil
+ },
+
+ // next tx
+ "right": func(ev *tcell.EventKey) *tcell.EventKey {
+ if d.Mach.Not1(ss.ClientSelected) {
+ return nil
+ }
+ if d.throttleKey(ev, arrowThrottleMs) {
+ // TODO fast jump scroll while holding the key
+ return nil
+ }
+
+ // scroll matrix
+ if d.Mach.Is1(ss.MatrixFocused) {
+ return ev
+ }
+
+ // scroll timelines
+ if d.Mach.Is1(ss.TimelineStepsFocused) {
+ // TODO try mach.IsScheduled(ss.UserFwdStep, am.MutationTypeAdd)
+ d.Mach.Add1(ss.UserFwdStep, nil)
+ } else {
+ d.Mach.Add1(ss.UserFwd, nil)
+ }
+
+ return nil
+ },
+
+ // state jumps
+ "alt+h": d.jumpBack,
+ "alt+l": d.jumpFwd,
+ "alt+Left": d.jumpBack,
+ "alt+Right": d.jumpFwd,
+
+ // page up / down
+ "alt+j": func(ev *tcell.EventKey) *tcell.EventKey {
+ return tcell.NewEventKey(tcell.KeyPgDn, ' ', tcell.ModNone)
+ },
+ "alt+k": func(ev *tcell.EventKey) *tcell.EventKey {
+ return tcell.NewEventKey(tcell.KeyPgUp, ' ', tcell.ModNone)
+ },
+
+ // expand / collapse the tree root
+ "alt+e": func(ev *tcell.EventKey) *tcell.EventKey {
+ expanded := false
+ children := d.tree.GetRoot().GetChildren()
+
+ for _, child := range children {
+ if child.IsExpanded() {
+ expanded = true
+ break
+ }
+ child.Collapse()
+ }
+
+ for _, child := range children {
+ if expanded {
+ child.Collapse()
+ child.GetReference().(*nodeRef).expanded = false
+ } else {
+ child.Expand()
+ child.GetReference().(*nodeRef).expanded = true
+ }
+ }
+
+ return nil
+ },
+
+ // tail mode
+ "alt+v": func(ev *tcell.EventKey) *tcell.EventKey {
+ d.Mach.Add1(ss.TailMode, nil)
+
+ return nil
+ },
+
+ // matrix view
+ "alt+m": func(ev *tcell.EventKey) *tcell.EventKey {
+ if d.Mach.Is1(ss.TreeLogView) {
+ d.Mach.Add1(ss.MatrixView, nil)
+ } else if d.Mach.Is1(ss.MatrixView) {
+ d.Mach.Add1(ss.TreeMatrixView, nil)
+ } else {
+ d.Mach.Add1(ss.TreeLogView, nil)
+ }
+
+ return nil
+ },
+
+ // scroll to the first tx
+ "home": func(ev *tcell.EventKey) *tcell.EventKey {
+ if d.Mach.Not1(ss.ClientSelected) {
+ return nil
+ }
+ d.C.CursorTx = 0
+ d.RedrawFull(true)
+
+ return nil
+ },
+
+ // scroll to the last tx
+ "end": func(ev *tcell.EventKey) *tcell.EventKey {
+ if d.Mach.Not1(ss.ClientSelected) {
+ return nil
+ }
+ d.C.CursorTx = len(d.C.MsgTxs)
+ d.RedrawFull(true)
+
+ return nil
+ },
+
+ // quit the app
+ "ctrl+q": func(ev *tcell.EventKey) *tcell.EventKey {
+ d.Mach.Remove1(ss.Start, nil)
+
+ return nil
+ },
+
+ // help modal
+ "?": func(ev *tcell.EventKey) *tcell.EventKey {
+ if d.Mach.Not1(ss.HelpDialog) {
+ d.Mach.Add1(ss.HelpDialog, nil)
+ } else {
+ d.Mach.Remove(ss.GroupDialog, nil)
+ }
+
+ return ev
+ },
+
+ // export modal
+ "alt+s": func(ev *tcell.EventKey) *tcell.EventKey {
+ if d.Mach.Not1(ss.ExportDialog) {
+ d.Mach.Add1(ss.ExportDialog, nil)
+ } else {
+ d.Mach.Remove(ss.GroupDialog, nil)
+ }
+
+ return ev
+ },
+
+ // exit modals
+ "esc": func(ev *tcell.EventKey) *tcell.EventKey {
+ if d.Mach.Any1(ss.GroupDialog...) {
+ d.Mach.Remove(ss.GroupDialog, nil)
+ return nil
+ }
+
+ return ev
+ },
+
+ // remove client (sidebar)
+ "backspace": func(ev *tcell.EventKey) *tcell.EventKey {
+ if d.Mach.Not1(ss.SidebarFocused) {
+ return ev
+ }
+
+ sel := d.sidebar.GetCurrentItem()
+ if sel == nil || d.Mach.Not1(ss.SidebarFocused) {
+ return nil
+ }
+
+ cid := sel.GetReference().(string)
+ d.Mach.Add1(ss.RemoveClient, am.A{"Client.id": cid})
+
+ return nil
+ },
+
+ // scroll to LogScrolled
+ // scroll sidebar
+ "down": func(ev *tcell.EventKey) *tcell.EventKey {
+ if d.Mach.Is1(ss.SidebarFocused) {
+ // TODO state?
+ go func() {
+ d.updateSidebar(true)
+ d.draw()
+ }()
+ } else if d.Mach.Is1(ss.LogFocused) {
+ d.Mach.Add1(ss.LogUserScrolled, nil)
+ }
+
+ return ev
+ },
+ "up": func(ev *tcell.EventKey) *tcell.EventKey {
+ if d.Mach.Is1(ss.SidebarFocused) {
+ // TODO state?
+ go func() {
+ d.updateSidebar(true)
+ d.draw()
+ }()
+ } else if d.Mach.Is1(ss.LogFocused) {
+ d.Mach.Add1(ss.LogUserScrolled, nil)
+ }
+
+ return ev
+ },
+ }
+}
+
+// TODO optimize usage places
+func (d *Debugger) throttleKey(ev *tcell.EventKey, ms int) bool {
+ // throttle
+ sameKey := d.lastKey == ev.Key()
+ elapsed := time.Since(d.lastKeyTime)
+ if sameKey && elapsed < time.Duration(ms)*time.Millisecond {
+ return true
+ }
+
+ d.lastKey = ev.Key()
+ d.lastKeyTime = time.Now()
+
+ return false
+}
+
+func (d *Debugger) jumpBack(ev *tcell.EventKey) *tcell.EventKey {
+ if d.throttleKey(ev, arrowThrottleMs) {
+ return nil
+ }
+
+ if d.Mach.Is1(ss.StateNameSelected) {
+ // state jump
+ d.ScrollToStateTx(d.C.selectedState, false)
+ } else {
+ // fast jump
+ d.Mach.Add1(ss.Back, am.A{"amount": min(fastJumpAmount, d.C.CursorTx)})
+ }
+
+ return nil
+}
+
+func (d *Debugger) jumpFwd(ev *tcell.EventKey) *tcell.EventKey {
+ if d.throttleKey(ev, arrowThrottleMs) {
+ return nil
+ }
+
+ if d.Mach.Is1(ss.StateNameSelected) {
+ // state jump
+ d.ScrollToStateTx(d.C.selectedState, true)
+ } else {
+ // fast jump
+ d.Mach.Add1(ss.Fwd, am.A{
+ "amount": min(fastJumpAmount, len(d.C.MsgTxs)-d.C.CursorTx),
+ })
+ }
+
+ return nil
+}
+
+// TODO verify host token, to distinguish 2 hosts with the same ID
+func (d *Debugger) parseClientMsg(c *Client, idx int) {
+ msgTxParsed := MsgTxParsed{}
+ msgTx := c.MsgTxs[idx]
+
+ // added / removed
+ if len(c.MsgTxs) > 1 && idx > 0 {
+ prevTx := c.MsgTxs[idx-1]
+ index := c.MsgStruct.StatesIndex
+
+ for i, name := range index {
+ if prevTx.Is1(index, name) && !msgTx.Is1(index, name) {
+ msgTxParsed.StatesRemoved = append(msgTxParsed.StatesRemoved, name)
+ } else if !prevTx.Is1(index, name) && msgTx.Is1(index, name) {
+ msgTxParsed.StatesAdded = append(msgTxParsed.StatesAdded, name)
+ } else if prevTx.Clocks[i] != msgTx.Clocks[i] {
+ // treat multi states as added
+ msgTxParsed.StatesAdded = append(msgTxParsed.StatesAdded, name)
+ }
+ }
+ }
+
+ // touched
+ touched := am.S{}
+ for _, step := range msgTx.Steps {
+ if step.FromState != "" {
+ touched = append(touched, step.FromState)
+ }
+ if step.ToState != "" {
+ touched = append(touched, step.ToState)
+ }
+ }
+ msgTxParsed.StatesTouched = lo.Uniq(touched)
+
+ // store the parsed msg
+ c.msgTxsParsed = append(c.msgTxsParsed, msgTxParsed)
+ // TODO take from msgs
+ c.lastActive = time.Now()
+
+ // pre-log entries
+ logStr := ""
+ for _, entry := range msgTx.PreLogEntries {
+ logStr += fmtLogEntry(entry, c.MsgStruct.States)
+ }
+
+ // tx log entries
+ if len(msgTx.LogEntries) > 0 {
+ f := ""
+ for _, entry := range msgTx.LogEntries {
+ f += fmtLogEntry(entry, c.MsgStruct.States)
+ }
+ // create a highlight region
+ logStr += `["` + msgTx.ID + `"]` + f + `[""]`
+ }
+
+ // TODO highlight the selected state
+
+ // store the parsed log
+ c.logMsgs = append(c.logMsgs, logStr)
+}
+
+func (d *Debugger) appendLog(index int) error {
+ c := d.C
+ logStr := c.logMsgs[index]
+ if logStr == "" {
+ return nil
+ }
+ if index > 0 {
+ msgTime := c.MsgTxs[index].Time
+ prevMsgTime := c.MsgTxs[index-1].Time
+ if prevMsgTime.Second() != msgTime.Second() {
+ // grouping labels (per second)
+ // TODO [::-]
+ logStr = `[grey]` + msgTime.Format(timeFormat) + "[white]\n" + logStr
+ }
+ }
+ _, err := d.log.Write([]byte(logStr))
+ if err != nil {
+ return err
+ }
+
+ // prevent scrolling when not in tail mode
+ if d.Mach.Not(am.S{ss.TailMode, ss.LogUserScrolled}) {
+ d.log.ScrollToHighlight()
+ } else if d.Mach.Not1(ss.TailMode) {
+ d.log.ScrollToEnd()
+ }
+
+ return nil
+}
+
+func (d *Debugger) draw() {
+ if d.repaintScheduled {
+ return
+ }
+ d.repaintScheduled = true
+
+ go func() {
+ // debounce every 16msec
+ time.Sleep(16 * time.Millisecond)
+ // TODO re-draw only changed components
+ d.app.QueueUpdateDraw(func() {})
+ d.repaintScheduled = false
+ }()
+}
+
+// TODO highlight selected state names, extract common logic
+func (d *Debugger) updateTxBars() {
+ d.currTxBarLeft.Clear()
+ d.currTxBarRight.Clear()
+ d.nextTxBarLeft.Clear()
+ d.nextTxBarRight.Clear()
+
+ if d.Mach.Not(am.S{ss.SelectingClient, ss.ClientSelected}) {
+ d.currTxBarLeft.SetText("Listening for connections on " + d.URL)
+ return
+ }
+
+ c := d.C
+ tx := d.CurrentTx()
+ if tx == nil {
+ // c is nil when switching clients
+ if c == nil || len(c.MsgTxs) == 0 {
+ d.currTxBarLeft.SetText("No transitions yet...")
+ } else {
+ d.currTxBarLeft.SetText("Initial structure")
+ }
+ } else {
+
+ var title string
+ switch d.Mach.Switch(ss.GroupPlaying...) {
+ case ss.Playing:
+ title = formatTxBarTitle("Playing")
+ case ss.TailMode:
+ title += formatTxBarTitle("Tail") + " "
+ default:
+ title = formatTxBarTitle("Paused") + " "
+ }
+
+ left, right := d.getTxInfo(c.CursorTx, tx, &c.msgTxsParsed[c.CursorTx-1],
+ title)
+ d.currTxBarLeft.SetText(left)
+ d.currTxBarRight.SetText(right)
+ }
+
+ nextTx := d.NextTx()
+ if nextTx != nil {
+ title := "Next "
+ left, right := d.getTxInfo(c.CursorTx+1, nextTx,
+ &c.msgTxsParsed[c.CursorTx], title)
+ d.nextTxBarLeft.SetText(left)
+ d.nextTxBarRight.SetText(right)
+ }
+}
+
+func (d *Debugger) updateLog(immediate bool) {
+ if immediate {
+ d.doUpdateLog()
+ return
+ }
+
+ if d.updateLogScheduled {
+ return
+ }
+ d.updateLogScheduled = true
+
+ go func() {
+ time.Sleep(logUpdateDebounce)
+ d.doUpdateLog()
+ d.draw()
+ d.updateLogScheduled = false
+ }()
+}
+
+func (d *Debugger) doUpdateLog() {
+ // check for a ready client
+ c := d.C
+ if c == nil {
+ return
+ }
+
+ if c.MsgStruct != nil {
+ lvl := c.MsgStruct.LogLevel
+ d.log.SetTitle(" Log:" + lvl.String() + " ")
+ }
+
+ // highlight the next tx if scrolling by steps
+ bySteps := d.Mach.Is1(ss.TimelineStepsFocused)
+ tx := d.CurrentTx()
+ if bySteps {
+ tx = d.NextTx()
+ }
+ if tx == nil {
+ d.log.Highlight("")
+ if bySteps {
+ d.log.ScrollToEnd()
+ } else {
+ d.log.ScrollToBeginning()
+ }
+
+ return
+ }
+
+ // highlight this tx or the prev if empty
+ if len(tx.LogEntries) == 0 && d.PrevTx() != nil {
+ last := d.PrevTx()
+ for i := d.C.CursorTx - 1; i >= 0; i-- {
+ if len(last.LogEntries) > 0 {
+ tx = last
+ break
+ }
+ last = d.C.MsgTxs[i-1]
+ }
+
+ d.log.Highlight(last.ID)
+ } else {
+ d.log.Highlight(tx.ID)
+ }
+
+ // scroll, but only if not manually scrolled
+ if d.Mach.Not1(ss.LogUserScrolled) {
+ d.log.ScrollToHighlight()
+ }
+}
+
+func (d *Debugger) updateTimelines() {
+ // check for a ready client
+ c := d.C
+ if c == nil {
+ return
+ }
+
+ txCount := len(c.MsgTxs)
+ nextTx := d.NextTx()
+ d.timelineSteps.SetTitleColor(cview.Styles.PrimaryTextColor)
+ d.timelineSteps.SetBorderColor(cview.Styles.PrimaryTextColor)
+ d.timelineSteps.SetFilledColor(cview.Styles.PrimaryTextColor)
+
+ // grey rejected bars
+ if nextTx != nil && !nextTx.Accepted {
+ d.timelineSteps.SetFilledColor(tcell.ColorGray)
+ }
+
+ // mark last step of a cancelled tx in red
+ if nextTx != nil && c.CursorStep == len(nextTx.Steps) && !nextTx.Accepted {
+ d.timelineSteps.SetFilledColor(tcell.ColorRed)
+ }
+
+ // inactive steps bar when no next tx
+ if nextTx == nil {
+ d.timelineSteps.SetTitleColor(tcell.ColorGrey)
+ d.timelineSteps.SetBorderColor(tcell.ColorGrey)
+ }
+ stepsCount := 0
+ onLastTx := c.CursorTx >= txCount
+ if !onLastTx {
+ stepsCount = len(c.MsgTxs[c.CursorTx].Steps)
+ }
+
+ // progressbar cant be max==0
+ d.timelineTxs.SetMax(max(txCount, 1))
+ // progress <= max
+ d.timelineTxs.SetProgress(c.CursorTx)
+ d.timelineTxs.SetTitle(d.P.Sprintf(
+ " Transition %d / %d ", c.CursorTx, txCount))
+
+ // progressbar cant be max==0
+ d.timelineSteps.SetMax(max(stepsCount, 1))
+ // progress <= max
+ d.timelineSteps.SetProgress(c.CursorStep)
+ d.timelineSteps.SetTitle(fmt.Sprintf(
+ " Next mutation step %d / %d ", c.CursorStep, stepsCount))
+}
+
+func (d *Debugger) updateKeyBars() {
+ // TODO light mode
+ keys := "[yellow]space: play/pause | left/right: back/fwd | " +
+ "alt+left/right/h/l: fast/state jump | home/end: start/end | " +
+ "alt+e/enter: expand/collapse | tab: focus | alt+v: tail mode | " +
+ "alt+m: matrix view | alt+s: export | ?: help"
+ d.keystrokesBar.SetText(keys)
+}
+
+func (d *Debugger) updateSidebar(immediate bool) {
+ if immediate {
+ d.doUpdateSidebar()
+ return
+ }
+
+ if d.updateSidebarScheduled {
+ // debounce non-forced updates
+ return
+ }
+ d.updateSidebarScheduled = true
+
+ go func() {
+ time.Sleep(sidebarUpdateDebounce)
+ if !d.updateSidebarScheduled {
+ return
+ }
+ d.doUpdateSidebar()
+ }()
+}
+
+// TODO sometimes scrolls for no reason
+func (d *Debugger) doUpdateSidebar() {
+ if d.Mach.Disposed {
+ return
+ }
+
+ for i, item := range d.sidebar.GetItems() {
+ name := item.GetReference().(string)
+ c := d.Clients[name]
+ label := d.getSidebarLabel(name, c, i)
+ item.SetMainText(label)
+ }
+
+ d.sidebar.SetTitle(" Machines:" + strconv.Itoa(len(d.Clients)) + " ")
+ d.updateSidebarScheduled = false
+ d.draw()
+}
+
+// buildSidebar builds the sidebar with the list of clients.
+// selectedIndex: index of the selected item, -1 for the current one.
+func (d *Debugger) buildSidebar(selectedIndex int) {
+ selected := ""
+ var item *cview.ListItem
+ if selectedIndex == -1 {
+ item = d.sidebar.GetCurrentItem()
+ } else if selectedIndex > -1 {
+ item = d.sidebar.GetItem(selectedIndex)
+ }
+ if item != nil {
+ selected = item.GetReference().(string)
+ }
+ d.sidebar.Clear()
+ var list []string
+ for _, c := range d.Clients {
+ list = append(list, c.id)
+ }
+
+ // sort a-z, with value numbers
+ humanSort(list)
+
+ for i, name := range list {
+ c := d.Clients[name]
+ label := d.getSidebarLabel(name, c, i)
+
+ // create list item
+ item := cview.NewListItem(label)
+ item.SetReference(name)
+ d.sidebar.AddItem(item)
+
+ if selected == "" && d.C != nil && d.C.id == name {
+ // pre-select the current machine
+ d.sidebar.SetCurrentItem(i)
+ } else if selected == name {
+ // select the previously selected one
+ d.sidebar.SetCurrentItem(i)
+ }
+ }
+
+ d.sidebar.SetTitle(" Machines:" + strconv.Itoa(len(d.Clients)) + " ")
+}
+
+func (d *Debugger) getSidebarLabel(name string, c *Client, index int) string {
+ label := d.P.Sprintf("%s (%d)", name, len(c.MsgTxs))
+ if d.C != nil && name == d.C.id {
+ label = "[::bu]" + label
+ }
+
+ isSel := d.sidebar.GetCurrentItemIndex() == index
+ hasFocus := d.Mach.Is1(ss.SidebarFocused)
+ if !c.connected {
+ if isSel && !hasFocus {
+ label = "[grey]" + label
+ } else if !isSel {
+ label = "[grey]" + label
+ } else {
+ label = "[black]" + label
+ }
+ }
+
+ return label
+}
+
+func (d *Debugger) updateBorderColor() {
+ color := cview.ColorUnset
+ if d.C != nil && d.C.connected {
+ color = colorActive
+ }
+
+ for _, box := range d.focusable {
+ box.SetBorderColorFocused(color)
+ }
+}
+
+// TODO should be an async state
+func (d *Debugger) exportData(filename string) {
+ // validate the input
+ if filename == "" {
+ log.Printf("Error: export failed no filename")
+ return
+ }
+ if len(d.Clients) == 0 {
+ log.Printf("Error: export failed no clients")
+ return
+ }
+
+ // validate the path
+ cwd, err := os.Getwd()
+ if err != nil {
+ log.Printf("Error: export failed %s", err)
+ return
+ }
+ gobPath := path.Join(cwd, filename+".gob.bz2")
+ fw, err := os.Create(gobPath)
+ if err != nil {
+ log.Printf("Error: export failed %s", err)
+ return
+ }
+ defer fw.Close()
+
+ // prepare the format
+ data := make([]*Exportable, len(d.Clients))
+ i := 0
+ for _, c := range d.Clients {
+ data[i] = &c.Exportable
+ i++
+ }
+
+ // create a new bzip2 writer
+ bz2w, err := bzip2.NewWriter(fw, nil)
+ if err != nil {
+ log.Printf("Error: export failed %s", err)
+ return
+ }
+ defer bz2w.Close()
+
+ // encode
+ encoder := gob.NewEncoder(bz2w)
+ err = encoder.Encode(data)
+ if err != nil {
+ log.Printf("Error: export failed %s", err)
+ }
+}
+
+// TODO async state
+func (d *Debugger) ImportData(filename string) {
+ fr, err := os.Open(filename)
+ if err != nil {
+ log.Printf("Error: import failed %s", err)
+ return
+ }
+
+ // decompress bz2
+ bz2r, err := bzip2.NewReader(fr, nil)
+ if err != nil {
+ log.Printf("Error: import failed %s", err)
+ return
+ }
+
+ // decode gob
+ decoder := gob.NewDecoder(bz2r)
+ var res []*Exportable
+ err = decoder.Decode(&res)
+ if err != nil {
+ log.Printf("Error: import failed %s", err)
+ return
+ }
+
+ // parse the data
+ for _, data := range res {
+ id := data.MsgStruct.ID
+ d.Clients[id] = &Client{
+ id: id,
+ Exportable: *data,
+ }
+ for i := range data.MsgTxs {
+ d.parseClientMsg(d.Clients[id], i)
+ }
+ }
+}
+
+///// ///// ///// ///// /////
+///// UTILS /////
+///// ///// ///// ///// /////
+
+func fmtLogEntry(entry string, machStruct am.Struct) string {
+ if entry == "" {
+ return entry
+ }
+
+ prefixEnd := "[][white]"
+
+ // color the first brackets per each line
+ ret := ""
+ for _, s := range strings.Split(entry, "\n") {
+ s = strings.Replace(strings.Replace(s,
+ "]", prefixEnd, 1),
+ "[", "[yellow][", 1)
+ ret += s + "\n"
+ }
+
+ // highlight state names (in the msg body)
+ // TODO removes the last letter
+ idx := strings.Index(ret, prefixEnd)
+ prefix := ret[0 : idx+len(prefixEnd)]
+
+ // style state names, start from the longest ones
+ // TODO compile as regexp and limit to words only
+ toReplace := maps.Keys(machStruct)
+ slices.Sort(toReplace)
+ slices.Reverse(toReplace)
+ for _, name := range toReplace {
+ body := ret[idx+len(prefixEnd):]
+ body = strings.ReplaceAll(body, " "+name, " [::b]"+name+"[::-]")
+ body = strings.ReplaceAll(body, "+"+name, "+[::b]"+name+"[::-]")
+ body = strings.ReplaceAll(body, "-"+name, "-[::b]"+name+"[::-]")
+ body = strings.ReplaceAll(body, ","+name, ",[::b]"+name+"[::-]")
+ ret = prefix + strings.ReplaceAll(body, "("+name, "([::b]"+name+"[::-]")
+ }
+
+ return strings.Trim(ret, " \n ") + "\n"
+}
+
+func (d *Debugger) getTxInfo(txIndex int,
+ tx *telemetry.DbgMsgTx, parsed *MsgTxParsed, title string,
+) (string, string) {
+ // left side
+ left := title
+ if tx != nil {
+
+ prevT := uint64(0)
+ if txIndex > 1 {
+ prevT = d.C.MsgTxs[txIndex-2].TimeSum()
+ }
+
+ left += d.P.Sprintf(" | tx: %v", txIndex)
+ left += d.P.Sprintf(" | T: +%v", tx.TimeSum()-prevT)
+ left += " |"
+
+ multi := ""
+ if len(tx.CalledStates) == 1 &&
+ d.C.MsgStruct.States[tx.CalledStates[0]].Multi {
+ multi += " multi"
+ }
+
+ if !tx.Accepted {
+ left += "[grey]"
+ }
+ left += fmt.Sprintf(" %s%s: [::b]%s[::-]", tx.Type, multi,
+ strings.Join(tx.CalledStates, ", "))
+
+ if !tx.Accepted {
+ left += "[-]"
+ }
+ }
+
+ // right side
+ // TODO time to execute
+ right := " "
+ if tx.IsAuto {
+ right += "auto | "
+ }
+ if !tx.Accepted {
+ right += "rejected | "
+ }
+ right += fmt.Sprintf("add: %d | rm: %d | touch: %d | %s",
+ len(parsed.StatesAdded), len(parsed.StatesRemoved),
+ len(parsed.StatesTouched), tx.Time.Format(timeFormat),
+ )
+
+ return left, right
+}
+
+func (d *Debugger) doCleanOnConnect() bool {
+ if len(d.Clients) == 0 {
+ return false
+ }
+ var disconns []*Client
+ for _, c := range d.Clients {
+ if !c.connected {
+ disconns = append(disconns, c)
+ }
+ }
+ // if all disconnected, clean up
+ if len(disconns) == len(d.Clients) {
+ for _, c := range d.Clients {
+ // TODO cant be scheduled, as the client can connect in the meantime
+ // d.Add1(ss.RemoveClient, am.A{"Client.id": c.id})
+ delete(d.Clients, c.id)
+ c.msgTxsParsed = nil
+ c.logMsgs = nil
+ c.MsgTxs = nil
+ c.MsgStruct = nil
+ }
+ return true
+ }
+ return false
+}
+
+// TODO
+func (d *Debugger) updateMatrix() {
+ d.matrix.Clear()
+ d.matrix.SetTitle(" Matrix ")
+
+ c := d.C
+ if c == nil || d.C.CursorTx == 0 {
+ d.matrix.Clear()
+ return
+ }
+
+ index := c.MsgStruct.StatesIndex
+ var tx *telemetry.DbgMsgTx
+ var prevTx *telemetry.DbgMsgTx
+ if c.CursorStep == 0 {
+ tx = d.CurrentTx()
+ prevTx = d.PrevTx()
+ } else {
+ tx = d.NextTx()
+ prevTx = d.CurrentTx()
+ }
+ steps := tx.Steps
+
+ // show the current tx summary on step 0, and partial if cursor > 0
+ if c.CursorStep > 0 {
+ steps = steps[:c.CursorStep]
+ }
+
+ highlightIndex := -1
+
+ // called states
+ var called []int
+ for i, name := range index {
+
+ v := "0"
+ if slices.Contains(tx.CalledStates, name) {
+ v = "1"
+ called = append(called, i)
+ }
+ d.matrix.SetCellSimple(0, i, matrixCellVal(v))
+
+ // mark called states
+ if slices.Contains(tx.CalledStates, name) {
+ d.matrix.GetCell(0, i).SetAttributes(tcell.AttrBold | tcell.AttrUnderline)
+ }
+
+ // mark selected state
+ if d.C.selectedState == name {
+ d.matrix.GetCell(0, i).SetBackgroundColor(colorHighlight)
+ highlightIndex = i
+ }
+ }
+ matrixEmptyRow(d, 1, len(index), highlightIndex)
+
+ // ticks
+ sum := 0
+ for i, name := range index {
+
+ var pTick uint64
+ if prevTx != nil {
+ pTick = prevTx.Clock(index, name)
+ }
+ tick := tx.Clock(index, name)
+
+ v := tick - pTick
+ sum += int(v)
+ d.matrix.SetCellSimple(2, i, matrixCellVal(strconv.Itoa(int(v))))
+ cell := d.matrix.GetCell(2, i)
+
+ if v == 0 {
+ cell.SetTextColor(tcell.ColorGrey)
+ }
+
+ // mark called states
+ if slices.Contains(called, i) {
+ cell.SetAttributes(
+ tcell.AttrBold | tcell.AttrUnderline)
+ }
+
+ // mark selected state
+ if d.C.selectedState == name {
+ cell.SetBackgroundColor(colorHighlight)
+ }
+ }
+ matrixEmptyRow(d, 3, len(index), highlightIndex)
+
+ // steps
+ for iRow, target := range index {
+ for iCol, source := range index {
+ v := 0
+
+ for _, step := range steps {
+
+ // TODO style just the cells
+ if step.FromState == source &&
+ ((step.ToState == "" && source == target) ||
+ step.ToState == target) {
+ v += int(step.Type)
+ }
+
+ strVal := strconv.Itoa(v)
+ strVal = matrixCellVal(strVal)
+ d.matrix.SetCellSimple(iRow+4, iCol, strVal)
+ cell := d.matrix.GetCell(iRow+4, iCol)
+
+ // mark selected state
+ if d.C.selectedState == target || d.C.selectedState == source {
+ cell.SetBackgroundColor(colorHighlight)
+ }
+
+ if v == 0 {
+ cell.SetTextColor(tcell.ColorGrey)
+ continue
+ }
+
+ // mark called states
+ if slices.Contains(called, iRow) || slices.Contains(called, iCol) {
+ cell.SetAttributes(tcell.AttrBold | tcell.AttrUnderline)
+ } else {
+ cell.SetAttributes(tcell.AttrBold)
+ }
+ }
+ }
+ }
+
+ d.matrix.SetTitle(" Matrix:" + strconv.Itoa(sum) + " ")
+}
+
+func matrixCellVal(strVal string) string {
+ switch len(strVal) {
+ case 1:
+ strVal = " " + strVal + " "
+ case 2:
+ strVal = " " + strVal
+ }
+ return strVal
+}
+
+func matrixEmptyRow(d *Debugger, row, colsCount, highlightIndex int) {
+ // empty row
+ for ii := 0; ii < colsCount; ii++ {
+ d.matrix.SetCellSimple(row, ii, " ")
+ if ii == highlightIndex {
+ d.matrix.GetCell(row, ii).SetBackgroundColor(colorHighlight)
+ }
+ }
+}
+
+func (d *Debugger) drawViews() {
+ d.updateViews(true)
+ d.updateFocusable()
+ d.draw()
+}
+
+func (d *Debugger) ConnectedClients() int {
+ // if only 1 client connected, select it (if SelectConnected == true)
+ var conns int
+ for _, c := range d.Clients {
+ if c.connected {
+ conns++
+ }
+ }
+
+ return conns
+}
+
+func (d *Debugger) getSidebarCurrClientIdx() int {
+ if d.C == nil {
+ return -1
+ }
+
+ i := 0
+ for _, item := range d.sidebar.GetItems() {
+ if item.GetReference().(string) == d.C.id {
+ return i
+ }
+ i++
+ }
+
+ return -1
+}
+
+func formatTxBarTitle(title string) string {
+ return "[::u]" + title + "[::-]"
+}
+
+var humanSortRE = regexp.MustCompile(`[0-9]+`)
+
+func humanSort(strs []string) {
+ sort.Slice(strs, func(i, j int) bool {
+ // skip overlapping parts
+ maxChars := min(len(strs[i]), len(strs[j]))
+ firstDiff := 0
+ for k := 0; k < maxChars; k++ {
+ if strs[i][k] != strs[j][k] {
+ break
+ }
+ firstDiff++
+ }
+
+ // if no numbers - compare as strings
+ posI := humanSortRE.FindStringIndex(strs[i][firstDiff:])
+ posJ := humanSortRE.FindStringIndex(strs[j][firstDiff:])
+ if len(posI) <= 0 || len(posJ) <= 0 || posI[0] != posJ[0] {
+ return strs[i] < strs[j]
+ }
+
+ // if contains numbers - sort by numbers
+ numsI := humanSortRE.FindAllString(strs[i][firstDiff:], -1)
+ numsJ := humanSortRE.FindAllString(strs[j][firstDiff:], -1)
+ numI, _ := strconv.Atoi(numsI[0])
+ numJ, _ := strconv.Atoi(numsJ[0])
+
+ if numI != numJ {
+ // If the numbers are different, order by the numbers
+ return numI < numJ
+ }
+
+ // If the numbers are the same, order lexicographically
+ return strs[i] < strs[j]
+ })
+}
diff --git a/tools/debugger/handlers.go b/tools/debugger/handlers.go
new file mode 100644
index 0000000..3e6c11b
--- /dev/null
+++ b/tools/debugger/handlers.go
@@ -0,0 +1,799 @@
+// TODO ExceptionState: separate error screen with stack trace
+
+package debugger
+
+import (
+ "context"
+ "slices"
+ "time"
+
+ "github.com/pancsta/cview"
+ "golang.org/x/exp/maps"
+ "golang.org/x/text/language"
+ "golang.org/x/text/message"
+
+ ss "github.com/pancsta/asyncmachine-go/tools/debugger/states"
+
+ am "github.com/pancsta/asyncmachine-go/pkg/machine"
+ "github.com/pancsta/asyncmachine-go/pkg/telemetry"
+)
+
+func (d *Debugger) StartEnd(_ *am.Event) {
+ d.app.Stop()
+}
+
+func (d *Debugger) StartState(e *am.Event) {
+ clientID, _ := e.Args["Client.id"].(string)
+ cursorTx, _ := e.Args["Client.cursorTx"].(int)
+ view, _ := e.Args["dbgView"].(string)
+
+ d.app = cview.NewApplication()
+ d.P = message.NewPrinter(language.English)
+ d.bindKeyboard()
+ d.initUIComponents()
+ d.initLayout()
+ if d.EnableMouse {
+ d.app.EnableMouse(true)
+ }
+
+ stateCtx := d.Mach.NewStateCtx(ss.Start)
+
+ // redraw on auto states
+ go func() {
+ // bind to transitions
+ txEndCh := d.Mach.OnEvent([]string{am.EventTransitionEnd}, nil)
+ for event := range txEndCh {
+
+ if stateCtx.Err() != nil {
+ return // expired
+ }
+
+ // TODO typesafe Args
+ tx := event.Args["transition"].(*am.Transition)
+ if tx.IsAuto() && tx.Accepted {
+ d.updateTxBars()
+ d.draw()
+ }
+ }
+ }()
+
+ // draw in a goroutine
+ go func() {
+ if stateCtx.Err() != nil {
+ return // expired
+ }
+
+ d.app.SetRoot(d.layoutRoot, true)
+ d.app.SetFocus(d.sidebar)
+ err := d.app.Run()
+ if err != nil {
+ d.Mach.AddErr(err)
+ }
+
+ d.Mach.Remove1(ss.Start, nil)
+ }()
+
+ // post-start ops
+ go func() {
+ if stateCtx.Err() != nil {
+ return // expired
+ }
+
+ // initial view from CLI
+ switch view {
+ case "tree-matrix":
+ d.Mach.Add1(ss.TreeMatrixView, nil)
+ case "matrix":
+ d.Mach.Add1(ss.MatrixView, nil)
+ }
+
+ // boot imported data
+ if len(d.Clients) <= 0 {
+ d.Mach.Add1(ss.Ready, nil)
+ return
+ }
+
+ d.buildSidebar(-1)
+ if clientID == "" {
+ ids := maps.Keys(d.Clients)
+ clientID = ids[0]
+ }
+ d.Mach.Add1(ss.SelectingClient, am.A{"Client.id": clientID})
+ <-d.Mach.When1(ss.ClientSelected, nil)
+
+ if stateCtx.Err() != nil {
+ return // expired
+ }
+
+ if cursorTx != 0 {
+ d.Mach.Add1(ss.ScrollToTx, am.A{"Client.cursorTx": cursorTx})
+ }
+
+ d.Mach.Add1(ss.Ready, nil)
+ }()
+}
+
+func (d *Debugger) StateNameSelectedState(e *am.Event) {
+ d.C.selectedState = e.Args["selectedStateName"].(string)
+ switch d.Mach.Switch(ss.GroupViews...) {
+
+ case ss.TreeLogView:
+ d.updateTree()
+
+ case ss.TreeMatrixView:
+ d.updateTree()
+ d.updateMatrix()
+
+ case ss.MatrixView:
+ d.updateMatrix()
+ }
+
+ d.updateKeyBars()
+}
+
+// StateNameSelectedStateNameSelected handles cursor moving from a state name to
+// another state name case.
+func (d *Debugger) StateNameSelectedStateNameSelected(e *am.Event) {
+ d.StateNameSelectedState(e)
+}
+
+func (d *Debugger) StateNameSelectedEnd(_ *am.Event) {
+ d.C.selectedState = ""
+ d.updateTree()
+ d.updateKeyBars()
+}
+
+func (d *Debugger) LiveViewEnter(_ *am.Event) bool {
+ return d.C != nil
+}
+
+func (d *Debugger) LiveViewState(_ *am.Event) {
+ d.C.CursorTx = len(d.C.MsgTxs)
+ d.C.CursorStep = 0
+ d.updateTxBars()
+ d.updateTimelines()
+ d.draw()
+}
+
+func (d *Debugger) PlayingState(_ *am.Event) {
+ if d.playTimer == nil {
+ d.playTimer = time.NewTicker(playInterval)
+ } else {
+ // TODO dont reset if resuming after switching clients
+ d.playTimer.Reset(playInterval)
+ }
+
+ // initial play step
+ if d.Mach.Is1(ss.TimelineStepsFocused) {
+ d.Mach.Add1(ss.FwdStep, nil)
+ } else {
+ d.Mach.Add1(ss.Fwd, nil)
+ }
+
+ ctx := d.Mach.NewStateCtx(ss.Playing)
+ go func() {
+ for ctx.Err() == nil {
+ select {
+ case <-ctx.Done(): // expired
+
+ case <-d.playTimer.C:
+
+ if d.Mach.Is1(ss.TimelineStepsFocused) {
+ d.Mach.Add1(ss.FwdStep, nil)
+ } else {
+ d.Mach.Add1(ss.Fwd, nil)
+ }
+ }
+ }
+ }()
+}
+
+func (d *Debugger) PlayingEnd(_ *am.Event) {
+ d.playTimer.Stop()
+}
+
+func (d *Debugger) PausedState(_ *am.Event) {
+ // TODO stop scrolling the log when coming from TailMode (confirm)
+ d.updateTxBars()
+ d.draw()
+}
+
+func (d *Debugger) TailModeState(_ *am.Event) {
+ // needed bc tail mode if carried over via SelectingClient
+ d.updateTxBars()
+ d.draw()
+}
+
+///// FWD / BACK
+
+func (d *Debugger) UserFwdState(e *am.Event) {
+ d.Mach.Remove1(ss.UserFwd, nil)
+}
+
+func (d *Debugger) FwdEnter(e *am.Event) bool {
+ amount, _ := e.Args["amount"].(int)
+ amount = max(amount, 1)
+ return d.C.CursorTx+amount <= len(d.C.MsgTxs)
+}
+
+func (d *Debugger) FwdState(e *am.Event) {
+ d.Mach.Remove1(ss.Fwd, nil)
+
+ amount, _ := e.Args["amount"].(int)
+ amount = max(amount, 1)
+
+ d.C.CursorTx += amount
+ d.C.CursorStep = 0
+ if d.Mach.Is1(ss.Playing) && d.C.CursorTx == len(d.C.MsgTxs) {
+ d.Mach.Remove1(ss.Playing, nil)
+ }
+
+ d.RedrawFull(false)
+}
+
+func (d *Debugger) UserBackState(e *am.Event) {
+ d.Mach.Remove1(ss.UserBack, nil)
+}
+
+func (d *Debugger) BackEnter(e *am.Event) bool {
+ amount, _ := e.Args["amount"].(int)
+ amount = max(amount, 1)
+ return d.C.CursorTx-amount >= 0
+}
+
+func (d *Debugger) BackState(e *am.Event) {
+ d.Mach.Remove1(ss.Back, nil)
+
+ amount, _ := e.Args["amount"].(int)
+ amount = max(amount, 1)
+
+ d.C.CursorTx -= amount
+ d.C.CursorStep = 0
+
+ d.RedrawFull(false)
+}
+
+///// STEP BACK / FWD
+
+func (d *Debugger) UserFwdStepState(e *am.Event) {
+ d.Mach.Remove1(ss.UserFwdStep, nil)
+}
+
+func (d *Debugger) FwdStepEnter(_ *am.Event) bool {
+ nextTx := d.NextTx()
+ if nextTx == nil {
+ return false
+ }
+ return d.C.CursorStep < len(nextTx.Steps)+1
+}
+
+func (d *Debugger) FwdStepState(_ *am.Event) {
+ d.Mach.Remove1(ss.FwdStep, nil)
+
+ // next tx
+ nextTx := d.NextTx()
+ // scroll to the next tx
+ if d.C.CursorStep == len(nextTx.Steps) {
+ d.Mach.Add1(ss.Fwd, nil)
+ return
+ }
+ d.C.CursorStep++
+ d.RedrawFull(false)
+}
+
+func (d *Debugger) UserBackStepState(e *am.Event) {
+ d.Mach.Remove1(ss.UserBackStep, nil)
+}
+
+func (d *Debugger) BackStepEnter(_ *am.Event) bool {
+ return d.C.CursorStep > 0 || d.C.CursorTx > 0
+}
+
+func (d *Debugger) BackStepState(_ *am.Event) {
+ d.Mach.Remove1(ss.BackStep, nil)
+
+ // wrap if there's a prev tx
+ if d.C.CursorStep <= 0 {
+ d.C.CursorTx--
+ d.updateLog(false)
+ nextTx := d.NextTx()
+ d.C.CursorStep = len(nextTx.Steps)
+ } else {
+ d.C.CursorStep--
+ }
+
+ d.RedrawFull(false)
+}
+
+func (d *Debugger) TimelineStepsFocusedState(_ *am.Event) {
+ d.RedrawFull(false)
+}
+
+func (d *Debugger) TimelineStepsFocusedEnd(_ *am.Event) {
+ d.RedrawFull(false)
+}
+
+///// CONNECTION
+
+func (d *Debugger) ConnectEventEnter(e *am.Event) bool {
+ _, ok1 := e.Args["msg_struct"].(*telemetry.DbgMsgStruct)
+ _, ok2 := e.Args["conn_id"].(string)
+ if !ok1 || !ok2 {
+ d.Mach.Log("Error: msg_struct malformed\n")
+ return false
+ }
+ return true
+}
+
+func (d *Debugger) ConnectEventState(e *am.Event) {
+ // initial structure data
+ msg := e.Args["msg_struct"].(*telemetry.DbgMsgStruct)
+ connID := e.Args["conn_id"].(string)
+ c := d.Clients[msg.ID]
+
+ // cleanup removes all previous clients, if all are disconnected
+ cleanup := false
+ if d.CleanOnConnect {
+ // remove old clients
+ cleanup = d.doCleanOnConnect()
+ }
+
+ if c == nil {
+ // new client
+ c = &Client{
+ id: msg.ID,
+ connID: connID,
+ connected: true,
+ Exportable: Exportable{
+ MsgStruct: msg,
+ },
+ }
+
+ d.Clients[msg.ID] = c
+ if !cleanup {
+ d.buildSidebar(-1)
+ }
+
+ } else {
+ // update the existing client
+ c = &Client{
+ id: msg.ID,
+ connID: connID,
+ connected: true,
+ Exportable: Exportable{
+ MsgStruct: msg,
+ },
+ }
+ d.Clients[msg.ID] = c
+
+ // currently selected - refresh the UI
+ if !cleanup {
+ if d.C != nil && d.C.id == msg.ID {
+ d.C = c
+ d.log.Clear()
+ d.updateTimelines()
+ d.updateTxBars()
+ // update the tree in case the struct changed
+ d.buildStatesTree()
+ d.updateBorderColor()
+ }
+ d.updateSidebar(true)
+ }
+ }
+
+ // rebuild the UI in case of a cleanup
+ if cleanup {
+ // select the new (and only) client
+ d.C = c
+ d.log.Clear()
+ d.updateTimelines()
+ d.updateTxBars()
+ d.updateBorderColor()
+ d.buildSidebar(0)
+ // initial build of the states tree
+ d.buildStatesTree()
+ d.updateViews(false)
+ }
+
+ // remove the last active client if over the limit
+ // TODO prioritize disconns
+ if len(d.Clients) > maxClients {
+ var (
+ lastActiveTime time.Time
+ lastActiveID string
+ )
+ // TODO get time from msgs
+ for id, c := range d.Clients {
+ if c.lastActive.After(lastActiveTime) || lastActiveID == "" {
+ lastActiveTime = c.lastActive
+ lastActiveID = id
+ }
+ }
+ d.Mach.Add1(ss.RemoveClient, am.A{"Client.id": lastActiveID})
+ }
+
+ // if only 1 client connected, select it
+ // if the only client in total, select it
+ if len(d.Clients) == 1 || (d.SelectConnected && d.ConnectedClients() == 1) {
+ d.Mach.Add1(ss.SelectingClient, am.A{
+ "Client.id": msg.ID,
+ // mark the origin
+ "from_connected": true,
+ })
+ }
+
+ // first client, tail mode
+ if len(d.Clients) == 1 {
+ d.Mach.Add1(ss.TailMode, nil)
+ }
+
+ d.draw()
+}
+
+func (d *Debugger) DisconnectEventEnter(e *am.Event) bool {
+ _, ok := e.Args["conn_id"].(string)
+ if !ok {
+ d.Mach.Log("Error: DisconnectEvent malformed\n")
+ return false
+ }
+
+ return true
+}
+
+func (d *Debugger) DisconnectEventState(e *am.Event) {
+ d.Mach.Remove1(ss.DisconnectEvent, nil)
+
+ connID := e.Args["conn_id"].(string)
+ for _, c := range d.Clients {
+ if c.connID != "" && c.connID == connID {
+ // mark as disconnected
+ c.connected = false
+ break
+ }
+ }
+
+ d.updateBorderColor()
+ d.updateSidebar(true)
+ d.draw()
+}
+
+///// CLIENTS
+
+func (d *Debugger) ClientMsgEnter(e *am.Event) bool {
+ _, ok := e.Args["msgs_tx"].([]*telemetry.DbgMsgTx)
+ if !ok {
+ d.Mach.Log("Error: msg_tx malformed\n")
+ return false
+ }
+
+ return true
+}
+
+func (d *Debugger) ClientMsgState(e *am.Event) {
+ msgs := e.Args["msgs_tx"].([]*telemetry.DbgMsgTx)
+
+ // add a timestamp
+ updateLive := false
+ updateFirstTx := false
+ for _, msg := range msgs {
+
+ c := d.Clients[msg.MachineID]
+ if _, ok := d.Clients[msg.MachineID]; !ok {
+ d.Mach.Log("Error: client not found: %s\n", msg.MachineID)
+ continue
+ }
+
+ if c.MsgStruct == nil {
+ d.Mach.Log("Error: struct missing for %s, ignoring tx\n", msg.MachineID)
+ continue
+ }
+
+ // TODO scalable storage
+ index := len(c.MsgTxs)
+ c.MsgTxs = append(c.MsgTxs, msg)
+ // parse the msg
+ d.parseClientMsg(c, len(c.MsgTxs)-1)
+
+ // update the UI
+ // TODO debounce UI updates
+
+ if c == d.C {
+ err := d.appendLog(index)
+ if err != nil {
+ d.Mach.Log("Error: log append %s\n", err)
+ // d.Mach.AddErr(err)
+ return
+ }
+ if d.Mach.Is1(ss.TailMode) {
+ updateLive = true
+ c.CursorTx = len(c.MsgTxs)
+ c.CursorStep = 0
+ }
+ // update Tx info on the first Tx
+ if len(c.MsgTxs) == 1 {
+ updateFirstTx = true
+ }
+ }
+ }
+
+ d.updateSidebar(false)
+ // UI updates for the selected client
+ if updateLive {
+ // force the latest tx
+ d.updateViews(false)
+ }
+
+ // update Tx info on the first Tx
+ if updateLive || updateFirstTx {
+ d.updateTxBars()
+ }
+
+ // timelines always change
+ d.updateTimelines()
+
+ d.draw()
+}
+
+func (d *Debugger) RemoveClientEnter(e *am.Event) bool {
+ cid, ok := e.Args["Client.id"].(string)
+ return ok && cid != ""
+}
+
+func (d *Debugger) RemoveClientState(e *am.Event) {
+ d.Mach.Remove1(ss.RemoveClient, nil)
+
+ cid := e.Args["Client.id"].(string)
+ c := d.Clients[cid]
+ if c == nil {
+ d.Mach.Log("Error: cant remove client %s: not found", cid)
+ return
+ }
+
+ // clean up
+ delete(d.Clients, cid)
+ c.MsgStruct = nil
+ c.MsgTxs = nil
+ c.logMsgs = nil
+ c.msgTxsParsed = nil
+ c.CursorTx = 0
+ c.CursorStep = 0
+
+ // if currently selected, switch to the first one
+ if c == d.C {
+ for id := range d.Clients {
+ d.Mach.Add1(ss.SelectingClient, am.A{"Client.id": id})
+ break
+ }
+ // if last client, unselect
+ if len(d.Clients) == 0 {
+ d.Mach.Remove1(ss.ClientSelected, nil)
+ }
+ d.buildSidebar(d.sidebar.GetCurrentItemIndex() - 1)
+ } else {
+ d.buildSidebar(-1)
+ }
+ d.draw()
+}
+
+// TODO SelectingClientSelectingClient
+
+func (d *Debugger) SelectingClientEnter(e *am.Event) bool {
+ cid, ok1 := e.Args["Client.id"].(string)
+ // same client
+ if d.C != nil && cid == d.C.id {
+ return false
+ }
+ // does the client exist?
+ _, ok2 := d.Clients[cid]
+
+ return len(d.Clients) > 0 && ok1 && ok2
+}
+
+func (d *Debugger) SelectingClientState(e *am.Event) {
+ clientID := e.Args["Client.id"].(string)
+ fromConnected, _ := e.Args["from_connected"].(bool)
+ fromPlaying := slices.Contains(e.Transition().StatesBefore, ss.Playing)
+
+ if d.Clients[clientID] == nil {
+ // TODO handle err, remove state
+ d.Mach.Log("Error: client not found: %s\n", clientID)
+
+ return
+ }
+
+ ctx := d.Mach.NewStateCtx(ss.SelectingClient)
+ // select the new default client
+ d.C = d.Clients[clientID]
+ // re-feed the whole log and pass the context to allow cancellation
+ logRebuildEnd := len(d.C.logMsgs)
+ d.logRebuildEnd = logRebuildEnd
+ // remain in TailMode after the selection
+ wasTailMode := slices.Contains(e.Transition().StatesBefore, ss.TailMode)
+
+ go func() {
+ if ctx.Err() != nil {
+ return // expired
+ }
+
+ // scroll to the same place as the prev client
+ match := false
+ if !wasTailMode {
+ for i, tx := range d.C.MsgTxs {
+ if tx.Time.After(d.prevClientTxTime) {
+
+ // pick the closer one
+ if i > 0 && tx.Time.Sub(d.prevClientTxTime) >
+ d.prevClientTxTime.Sub(*d.C.MsgTxs[i-1].Time) {
+ d.C.CursorTx = i
+ } else {
+ d.C.CursorTx = i + 1
+ }
+ match = true
+
+ break
+ }
+ }
+ }
+
+ // or scroll to the last one
+ if !match {
+ d.C.CursorTx = len(d.C.MsgTxs)
+ }
+ d.C.CursorStep = 0
+ d.updateTimelines()
+ d.updateTxBars()
+ d.updateSidebar(true)
+
+ // scroll into view
+ selOffset := d.getSidebarCurrClientIdx()
+ offset, _ := d.sidebar.GetOffset()
+ _, _, _, lines := d.sidebar.GetInnerRect()
+ if selOffset-offset > lines {
+ d.sidebar.SetCurrentItem(selOffset)
+ d.updateSidebar(false)
+ }
+
+ // initial build of the states tree
+ d.buildStatesTree()
+ if d.Mach.Is1(ss.TreeLogView) || d.Mach.Is1(ss.TreeMatrixView) {
+ d.updateTree()
+ }
+
+ // rebuild the whole log, keep an eye on the ctx
+ // TODO cache in single []byte
+ for i := 0; i < logRebuildEnd && ctx.Err() == nil; i++ {
+ err := d.appendLog(i)
+ if err != nil {
+ d.Mach.Log("Error: log rebuild %s\n", err)
+ }
+ }
+ if ctx.Err() != nil {
+ return // expired
+ }
+ d.draw()
+
+ target := am.S{ss.ClientSelected}
+ if wasTailMode {
+ target = append(target, ss.TailMode)
+ }
+
+ d.Mach.Add(target, am.A{
+ "ctx": ctx,
+ "from_connected": fromConnected,
+ "from_playing": fromPlaying,
+ })
+ }()
+}
+
+func (d *Debugger) ClientSelectedState(e *am.Event) {
+ ctx := e.Args["ctx"].(context.Context)
+ fromConnected, _ := e.Args["from_connected"].(bool)
+ fromPlaying, _ := e.Args["from_playing"].(bool)
+ if ctx.Err() != nil {
+ d.Mach.Log("Error: context expired\n")
+ return // expired
+ }
+
+ // catch up with new log msgs
+ for i := d.logRebuildEnd; i < len(d.C.logMsgs); i++ {
+ err := d.appendLog(i)
+ if err != nil {
+ d.Mach.Log("Error: log rebuild %s\n", err)
+ }
+ }
+
+ // update views
+ if d.Mach.Is1(ss.TreeLogView) {
+ d.updateLog(false)
+ }
+ if d.Mach.Is1(ss.MatrixView) || d.Mach.Is1(ss.TreeMatrixView) {
+ d.updateMatrix()
+ }
+
+ // first client connected, set tail mode
+ if fromConnected && len(d.Clients) == 1 {
+ d.Mach.Add1(ss.TailMode, nil)
+ } else if fromPlaying {
+ d.Mach.Add1(ss.Playing, nil)
+ }
+
+ d.updateBorderColor()
+
+ d.draw()
+}
+
+func (d *Debugger) ClientSelectedEnd(e *am.Event) {
+ // memorize the current tx time
+ if d.C != nil {
+ if d.C.CursorTx > 0 && d.C.CursorTx <= len(d.C.MsgTxs) {
+ d.prevClientTxTime = *d.C.MsgTxs[d.C.CursorTx-1].Time
+ }
+ }
+
+ // clean up, except when switching to SelectingClient
+ if !e.Mutation().StateWasCalled(ss.SelectingClient) {
+ d.C = nil
+ }
+
+ d.log.Clear()
+ d.treeRoot.ClearChildren()
+ d.RedrawFull(true)
+}
+
+func (d *Debugger) HelpDialogState(e *am.Event) {
+ // TODO use Visibility instead of SendToFront
+ d.layoutRoot.SendToFront("main")
+ d.layoutRoot.SendToFront("help")
+}
+
+func (d *Debugger) HelpDialogEnd(e *am.Event) {
+ diff := am.DiffStates(ss.GroupDialog, e.Transition().TargetStates)
+ if len(diff) == len(ss.GroupDialog) {
+ // all dialogs closed, show main
+ d.layoutRoot.SendToFront("main")
+ }
+}
+
+func (d *Debugger) ExportDialogState(e *am.Event) {
+ // TODO use Visibility instead of SendToFront
+ d.layoutRoot.SendToFront("main")
+ d.layoutRoot.SendToFront("export")
+}
+
+func (d *Debugger) ExportDialogEnd(e *am.Event) {
+ diff := am.DiffStates(ss.GroupDialog, e.Transition().TargetStates)
+ if len(diff) == len(ss.GroupDialog) {
+ // all dialogs closed, show main
+ d.layoutRoot.SendToFront("main")
+ }
+}
+
+func (d *Debugger) MatrixViewState(_ *am.Event) {
+ d.drawViews()
+}
+
+func (d *Debugger) MatrixViewEnd(_ *am.Event) {
+ d.drawViews()
+}
+
+func (d *Debugger) TreeMatrixViewState(_ *am.Event) {
+ d.drawViews()
+}
+
+func (d *Debugger) TreeMatrixViewEnd(_ *am.Event) {
+ d.drawViews()
+}
+
+func (d *Debugger) ScrollToTxEnter(e *am.Event) bool {
+ cursor, ok := e.Args["Client.cursorTx"].(int)
+ c := d.C
+ return ok && c != nil && len(c.MsgTxs) > cursor+1
+}
+
+func (d *Debugger) ScrollToTxState(e *am.Event) {
+ d.Mach.Remove1(ss.ScrollToTx, nil)
+
+ cursor := e.Args["Client.cursorTx"].(int)
+ d.C.CursorTx = cursor
+ d.RedrawFull(false)
+}
diff --git a/tools/debugger/rpc.go b/tools/debugger/rpc.go
new file mode 100644
index 0000000..1fce8c9
--- /dev/null
+++ b/tools/debugger/rpc.go
@@ -0,0 +1,124 @@
+package debugger
+
+import (
+ "encoding/gob"
+ "log"
+ "net"
+ "net/rpc"
+ "sync"
+ "time"
+
+ "github.com/soheilhy/cmux"
+
+ am "github.com/pancsta/asyncmachine-go/pkg/machine"
+ "github.com/pancsta/asyncmachine-go/pkg/telemetry"
+ ss "github.com/pancsta/asyncmachine-go/tools/debugger/states"
+)
+
+type RPCServer struct {
+ Mach *am.Machine
+ ConnID string
+}
+
+func (r *RPCServer) DbgMsgStruct(
+ msgStruct *telemetry.DbgMsgStruct, _ *string,
+) error {
+ r.Mach.Add1(ss.ConnectEvent, am.A{
+ "msg_struct": msgStruct,
+ "conn_id": r.ConnID,
+ })
+
+ return nil
+}
+
+var (
+ queue []*telemetry.DbgMsgTx
+ queueMx sync.Mutex
+ scheduled bool
+)
+
+func (r *RPCServer) DbgMsgTx(msgTx *telemetry.DbgMsgTx, _ *string) error {
+ queueMx.Lock()
+ defer queueMx.Unlock()
+
+ if !scheduled {
+ scheduled = true
+ go func() {
+ time.Sleep(time.Second)
+
+ queueMx.Lock()
+ defer queueMx.Unlock()
+
+ r.Mach.Add1(ss.ClientMsg, am.A{"msgs_tx": queue})
+ queue = nil
+ scheduled = false
+ }()
+ }
+
+ now := time.Now()
+ msgTx.Time = &now
+ queue = append(queue, msgTx)
+ return nil
+}
+
+func StartRCP(mach *am.Machine, url string) {
+ var err error
+ gob.Register(am.Relation(0))
+ if url == "" {
+ url = telemetry.DbgHost
+ }
+ lis, err := net.Listen("tcp", url)
+ if err != nil {
+ log.Println(err)
+ mach.AddErr(err)
+ // TODO nice err msg
+ panic(err)
+ }
+ log.Println("RPC server started at", url)
+ // Create a new cmux instance.
+ m := cmux.New(lis)
+
+ // Match connections in order: first rpc, then everything else.
+ rpcL := m.Match(cmux.Any())
+ go rpcAccept(rpcL, mach)
+
+ // Start cmux serving.
+ if err := m.Serve(); err != nil {
+ panic(err)
+ }
+}
+
+func rpcAccept(l net.Listener, mach *am.Machine) {
+ // TODO test if this fixes cmux
+ defer func() {
+ if r := recover(); r != nil {
+ log.Println("recovered:", r)
+ }
+ }()
+ for {
+ conn, err := l.Accept()
+ if err != nil {
+ log.Println(err)
+ mach.AddErr(err)
+ continue
+ }
+ // handle the client
+ go func() {
+ server := rpc.NewServer()
+ connID := conn.RemoteAddr().String()
+ rcvr := &RPCServer{
+ Mach: mach,
+ ConnID: connID,
+ }
+ err = server.Register(rcvr)
+ if err != nil {
+ log.Println(err)
+ rcvr.Mach.AddErr(err)
+ // TODO err msg
+ panic(err)
+ }
+ server.ServeConn(conn)
+ rcvr.Mach.Add1(ss.DisconnectEvent, am.A{"conn_id": connID})
+ }()
+ }
+}
diff --git a/tools/debugger/states/ss_dbg.go b/tools/debugger/states/ss_dbg.go
new file mode 100644
index 0000000..c5e1d00
--- /dev/null
+++ b/tools/debugger/states/ss_dbg.go
@@ -0,0 +1,232 @@
+package states
+
+import am "github.com/pancsta/asyncmachine-go/pkg/machine"
+
+// S is a type alias for a list of state names.
+type S = am.S
+
+// States map defines relations and properties of states.
+var States = am.Struct{
+
+ ///// Input events
+
+ ClientMsg: {Multi: true},
+ ConnectEvent: {Multi: true},
+ DisconnectEvent: {Multi: true},
+
+ // user scrolling tx / steps
+ UserFwd: {
+ Add: S{Fwd},
+ Remove: GroupPlaying,
+ },
+ UserBack: {
+ Add: S{Back},
+ Remove: GroupPlaying,
+ },
+ UserFwdStep: {
+ Add: S{FwdStep},
+ Require: S{ClientSelected},
+ Remove: am.SMerge(GroupPlaying, S{LogUserScrolled}),
+ },
+ UserBackStep: {
+ Add: S{BackStep},
+ Require: S{ClientSelected},
+ Remove: am.SMerge(GroupPlaying, S{LogUserScrolled}),
+ },
+
+ ///// External state (eg UI)
+
+ TreeFocused: {Remove: GroupFocused},
+ LogFocused: {Remove: GroupFocused},
+ SidebarFocused: {Remove: GroupFocused},
+ TimelineTxsFocused: {Remove: GroupFocused},
+ TimelineStepsFocused: {Remove: GroupFocused},
+ MatrixFocused: {Remove: GroupFocused},
+ DialogFocused: {Remove: GroupFocused},
+ StateNameSelected: {Require: S{ClientSelected}},
+ HelpDialog: {Remove: GroupDialog},
+ ExportDialog: {
+ Require: S{ClientSelected},
+ Remove: GroupDialog,
+ },
+ LogUserScrolled: {},
+ Ready: {Require: S{Start}},
+
+ ///// Actions
+
+ Start: {},
+ TreeLogView: {
+ Auto: true,
+ Remove: GroupViews,
+ },
+ MatrixView: {Remove: GroupViews},
+ TreeMatrixView: {Remove: GroupViews},
+ TailMode: {
+ Require: S{ClientSelected},
+ Remove: GroupPlaying,
+ },
+ Playing: {
+ Require: S{ClientSelected},
+ Remove: am.SMerge(GroupPlaying, S{LogUserScrolled}),
+ },
+ Paused: {
+ Auto: true,
+ Require: S{ClientSelected},
+ Remove: GroupPlaying,
+ },
+
+ // tx / steps back / fwd
+ Fwd: {
+ Require: S{ClientSelected},
+ Remove: S{Playing},
+ },
+ Back: {
+ Require: S{ClientSelected},
+ Remove: S{Playing},
+ },
+ FwdStep: {
+ Require: S{ClientSelected},
+ Remove: S{Playing},
+ },
+ BackStep: {
+ Require: S{ClientSelected},
+ Remove: S{Playing},
+ },
+
+ ScrollToTx: {Require: S{ClientSelected}},
+
+ // client
+ SelectingClient: {Remove: S{ClientSelected}},
+ ClientSelected: {
+ Remove: S{SelectingClient, LogUserScrolled},
+ },
+ RemoveClient: {Require: S{ClientSelected}},
+}
+
+// Groups of mutually exclusive states.
+
+var (
+ GroupFocused = S{
+ TreeFocused, LogFocused, TimelineTxsFocused,
+ TimelineStepsFocused, SidebarFocused, MatrixFocused, DialogFocused,
+ }
+ GroupPlaying = S{
+ Playing, Paused, TailMode,
+ }
+ GroupDialog = S{
+ HelpDialog, ExportDialog,
+ }
+ GroupViews = S{
+ MatrixView, TreeLogView, TreeMatrixView,
+ }
+)
+
+//#region boilerplate defs
+
+// Names of all the states (pkg enum).
+
+const (
+ TreeFocused = "TreeFocused"
+ LogFocused = "LogFocused"
+ TimelineTxsFocused = "TimelineTxsFocused"
+ TimelineStepsFocused = "TimelineStepsFocused"
+ MatrixFocused = "MatrixFocused"
+ DialogFocused = "DialogFocused"
+ ClientMsg = "ClientMsg"
+ StateNameSelected = "StateNameSelected"
+ Start = "Start"
+ Playing = "Playing"
+ Paused = "Paused"
+ // TailMode always shows the latest transition
+ TailMode = "TailMode"
+ // UserFwd is a user generated event
+ UserFwd = "UserFwd"
+ // Fwd moves to the next transition
+ Fwd = "Fwd"
+ // UserBack is a user generated event
+ UserBack = "UserBack"
+ // Back moves to the previous transition
+ Back = "Back"
+ // UserFwdStep is a user generated event
+ UserFwdStep = "UserFwdStep"
+ // UserBackStep is a user generated event
+ UserBackStep = "UserBackStep"
+ // FwdStep moves to the next transition's steps
+ FwdStep = "FwdStep"
+ // BackStep moves to the previous transition's steps
+ BackStep = "BackStep"
+ ConnectEvent = "ConnectEvent"
+ DisconnectEvent = "DisconnectEvent"
+ SidebarFocused = "SidebarFocused"
+ RemoveClient = "RemoveClient"
+ ClientSelected = "ClientSelected"
+ SelectingClient = "SelectingClient"
+ HelpDialog = "HelpDialog"
+ ExportDialog = "ExportDialog"
+ MatrixView = "MatrixView"
+ TreeLogView = "TreeLogView"
+ TreeMatrixView = "TreeMatrixView"
+ LogUserScrolled = "LogUserScrolled"
+ ScrollToTx = "ScrollToTx"
+ Ready = "Ready"
+)
+
+// Names of all the states (pkg enum).
+
+// Names is an ordered list of all the state names.
+var Names = S{
+
+ ///// Input events
+
+ ClientMsg,
+ ConnectEvent,
+ DisconnectEvent,
+
+ // user scrolling
+ UserFwd,
+ UserBack,
+ UserFwdStep,
+ UserBackStep,
+
+ ///// External state (eg UI)
+
+ TreeFocused,
+ LogFocused,
+ SidebarFocused,
+ TimelineTxsFocused,
+ TimelineStepsFocused,
+ MatrixFocused,
+ DialogFocused,
+ StateNameSelected,
+ HelpDialog,
+ ExportDialog,
+ LogUserScrolled,
+ Ready,
+
+ ///// Actions
+
+ Start,
+ TreeLogView,
+ MatrixView,
+ TreeMatrixView,
+ TailMode,
+ Playing,
+ Paused,
+
+ // tx / steps back / fwd
+ Fwd,
+ Back,
+ FwdStep,
+ BackStep,
+
+ ScrollToTx,
+
+ // client
+ ClientSelected,
+ SelectingClient,
+ RemoveClient,
+
+ am.Exception,
+}
+
+//#endregion
diff --git a/tools/debugger/tree.go b/tools/debugger/tree.go
new file mode 100644
index 0000000..7a7cf79
--- /dev/null
+++ b/tools/debugger/tree.go
@@ -0,0 +1,675 @@
+// TODO extract the tree logic to a separate struct, re-write
+
+package debugger
+
+import (
+ "fmt"
+ "slices"
+ "strconv"
+ "strings"
+
+ am "github.com/pancsta/asyncmachine-go/pkg/machine"
+ "github.com/pancsta/asyncmachine-go/pkg/telemetry"
+ ss "github.com/pancsta/asyncmachine-go/tools/debugger/states"
+
+ "github.com/gdamore/tcell/v2"
+ "github.com/pancsta/cview"
+)
+
+func (d *Debugger) initMachineTree() *cview.TreeView {
+ d.treeRoot = cview.NewTreeNode("")
+ d.treeRoot.SetColor(tcell.ColorRed)
+
+ tree := cview.NewTreeView()
+ tree.SetRoot(d.treeRoot)
+ tree.SetCurrentNode(d.treeRoot)
+ tree.SetHighlightColor(colorHighlight)
+
+ tree.SetChangedFunc(func(node *cview.TreeNode) {
+ ref, ok := node.GetReference().(*nodeRef)
+ if !ok {
+ d.Mach.Remove1(ss.StateNameSelected, nil)
+ return
+ }
+
+ if ref.stateName == "" {
+ d.Mach.Remove1(ss.StateNameSelected, nil)
+ return
+ }
+ d.Mach.Add1(ss.StateNameSelected, am.A{
+ "selectedStateName": ref.stateName,
+ })
+ })
+
+ tree.SetSelectedFunc(func(node *cview.TreeNode) {
+ ref, ok := node.GetReference().(*nodeRef)
+ if !ok {
+ // TODO err
+ return
+ }
+
+ // jump to referenced state
+ if ref.isRef && ref.stateName != "" {
+ name := normalizeText(ref.stateName)
+ for _, child := range d.treeRoot.GetChildren() {
+ if name == normalizeText(strings.Split(child.GetText(), " ")[0]) {
+ d.tree.SetCurrentNode(child)
+
+ return
+ }
+ }
+ }
+
+ ref.expanded = !node.IsExpanded()
+ node.SetExpanded(ref.expanded)
+ })
+
+ return tree
+}
+
+func (d *Debugger) updateTree() {
+ var msg telemetry.DbgMsg
+ c := d.C
+ if c == nil {
+ return
+ }
+
+ // debug
+ // log.Println("///// updateTree")
+
+ queue := ""
+ if c.CursorTx == 0 {
+ msg = c.MsgStruct
+ } else {
+ tx := c.MsgTxs[c.CursorTx-1]
+ msg = tx
+ queue = fmt.Sprintf("(Q:%d S:%d) ",
+ tx.Queue, len(c.MsgStruct.StatesIndex))
+ }
+
+ d.tree.SetTitle(" Structure " + queue)
+
+ var steps []*am.Step
+ if c.CursorTx < len(c.MsgTxs) && c.CursorStep > 0 {
+ steps = d.NextTx().Steps
+ }
+
+ // default decorations plus name highlights
+ colIdx := d.updateTreeDefaultsHighlights(msg)
+
+ // decorate steps, take the longest row from either defaults or steps
+ colIdx = max(colIdx, d.updateTreeTxSteps(steps))
+ colIdx += treeIndent
+ d.sortTree()
+ d.updateTreeRelCols(colIdx, steps)
+}
+
+// returns the length of the longest row
+// TODO refactor
+func (d *Debugger) updateTreeDefaultsHighlights(msg telemetry.DbgMsg) int {
+ c := d.C
+ if c == nil {
+ return 0
+ }
+
+ maxLen := 0
+
+ d.tree.GetRoot().WalkUnsafe(func(
+ node, parent *cview.TreeNode, depth int,
+ ) bool {
+ // skip the root
+ if parent == nil {
+ // get node text length
+ maxLen = maxNodeLen(node, maxLen, depth)
+ return true
+ }
+ ref, ok := node.GetReference().(*nodeRef)
+ if !ok {
+ // get node text length
+ maxLen = maxNodeLen(node, maxLen, depth)
+ return true
+ }
+
+ ref.touched = false
+ node.SetBold(false)
+ node.SetUnderline(false)
+
+ if ref.isRel {
+
+ node.SetText(capitalizeFirst(ref.rel.String()))
+ return true
+ } else if ref.isProp {
+
+ node.SetText(ref.propLabel)
+ // get node text length
+ maxLen = maxNodeLen(node, maxLen, depth)
+ return true
+ }
+
+ // inherit
+ if parent == d.tree.GetRoot() || !parent.GetHighlighted() {
+ node.SetHighlighted(false)
+ }
+
+ stateName := ref.stateName
+ color := colorInactive
+
+ if msg.Is(c.MsgStruct.StatesIndex, am.S{stateName}) {
+ color = colorActive
+ }
+
+ // reset to defaults
+ node.SetText(stateName)
+
+ // reset to defaults
+ if stateName != c.selectedState {
+ if !ref.isRef {
+ // un-highlight all descendants
+ for _, child := range node.GetChildren() {
+ child.SetHighlighted(false)
+ for _, child2 := range child.GetChildren() {
+ child2.SetHighlighted(false)
+ }
+ }
+
+ // TODO K delimiters
+ tick := strconv.FormatUint(msg.Clock(c.MsgStruct.StatesIndex,
+ stateName), 10)
+ node.SetColor(color)
+ node.SetText(stateName + " (" + tick + ")")
+ }
+
+ // get node text length
+ maxLen = maxNodeLen(node, maxLen, depth)
+
+ return true
+ }
+
+ // reference
+ if node != d.tree.GetCurrentNode() {
+ node.SetHighlighted(true)
+ // log.Println("highlight", stateName)
+ }
+ if ref.isRef {
+
+ // get node text length
+ maxLen = maxNodeLen(node, maxLen, depth)
+ return true
+ }
+
+ // top-level state
+ tick := strconv.FormatUint(msg.Clock(c.MsgStruct.StatesIndex,
+ stateName), 10)
+ node.SetColor(color)
+ node.SetText(stateName + " (" + tick + ")")
+
+ // get node text length
+ maxLen = maxNodeLen(node, maxLen, depth)
+
+ if node == d.tree.GetCurrentNode() {
+ return true
+ }
+
+ // highlight all descendants
+ for _, child := range node.GetChildren() {
+ child.SetHighlighted(true)
+ for _, child2 := range child.GetChildren() {
+ child2.SetHighlighted(true)
+ }
+ }
+
+ return true
+ })
+
+ return maxLen
+}
+
+func maxNodeLen(node *cview.TreeNode, maxLen int, depth int) int {
+ return max(maxLen, node.VisibleLength()+(depth-1)*3)
+}
+
+const treeIndent = 3
+
+func (d *Debugger) updateTreeTxSteps(steps []*am.Step) int {
+ c := d.C
+ if c == nil {
+ return 0
+ }
+
+ maxLen := 0
+
+ // walk the tree only when scrolling steps
+ if c.CursorStep < 1 {
+ for _, node := range d.tree.GetRoot().GetChildren() {
+ ref, ok := node.GetReference().(*nodeRef)
+ if !ok {
+ continue
+ }
+ d.handleExpanded(node, ref, c)
+ }
+ return 0
+ }
+
+ // get max length
+ d.tree.GetRoot().WalkUnsafe(func(
+ node, parent *cview.TreeNode, depth int,
+ ) bool {
+ maxLen = maxNodeLen(node, maxLen, depth)
+ return true
+ })
+
+ // current max len with step tags
+ maxLenTagged := maxLen
+
+ d.tree.GetRoot().WalkUnsafe(func(
+ node, parent *cview.TreeNode, depth int,
+ ) bool {
+ // skip the root
+ if parent == nil {
+ return true
+ }
+
+ ref, ok := node.GetReference().(*nodeRef)
+ if !ok {
+ return true
+ }
+
+ if ref.stateName != "" {
+
+ // STATE NAME NODES
+ stateName := ref.stateName
+ for i := range steps {
+
+ if c.CursorStep == i {
+ break
+ }
+ step := steps[i]
+ textMargin := ""
+ visibleLen := node.VisibleLength()
+ if maxLen+1-visibleLen > 0 {
+ textMargin = strings.Repeat(" ", maxLen+1-visibleLen)
+ // debug
+ // log.Printf("node: %s, textMargin: %d, depth: %d, visibleLen: %d",
+ // node.GetText(), len(textMargin), depth, visibleLen)
+ }
+
+ switch step.Type {
+ case am.StepRemoveNotActive:
+ if step.ToState == stateName && !ref.isRef {
+ node.SetBold(true)
+ ref.touched = true
+ }
+
+ case am.StepRemove:
+ if step.ToState == stateName && !ref.isRef {
+ node.SetText(node.GetText() + textMargin + "-")
+ node.SetBold(true)
+ ref.touched = true
+ }
+
+ case am.StepRelation:
+
+ if step.FromState == stateName && !ref.isRef {
+
+ node.SetBold(true)
+ ref.touched = true
+ } else if step.ToState == stateName && !ref.isRef {
+
+ node.SetBold(true)
+ ref.touched = true
+ } else if ref.isRef && step.ToState == stateName &&
+ ref.parentState == step.FromState {
+
+ node.SetBold(true)
+ ref.touched = true
+ }
+
+ case am.StepHandler:
+ if ref.isRef {
+ continue
+ }
+ // states handler executed
+ if step.FromState == stateName || step.ToState == stateName {
+ node.SetText(node.GetText() + textMargin + "*")
+ node.SetBold(true)
+ ref.touched = true
+ }
+
+ case am.StepSet:
+ if step.ToState == stateName && !ref.isRef {
+ node.SetText(node.GetText() + textMargin + "+")
+ node.SetBold(true)
+ ref.touched = true
+ }
+
+ case am.StepRequested:
+ if step.ToState == stateName && !ref.isRef {
+ node.SetText("[::u]" + node.GetText() + "[::-]")
+ node.SetBold(true)
+ ref.touched = true
+ }
+
+ case am.StepCancel:
+ if step.ToState == stateName && !ref.isRef {
+ node.SetText(node.GetText() + textMargin + "!")
+ node.SetBold(true)
+ ref.touched = true
+ }
+ }
+ }
+
+ d.handleExpanded(node, ref, c)
+ } else if ref.isRel {
+ // RELATION NODES
+ for i := range steps {
+
+ if c.CursorStep == i {
+ break
+ }
+
+ step := steps[i]
+ if step.Type != am.StepRelation {
+ continue
+ }
+
+ if step.Data == ref.rel && ref.parentState == step.FromState {
+ node.SetBold(true)
+ ref.touched = true
+ }
+ }
+ }
+
+ maxLenTagged = maxNodeLen(node, maxLenTagged, depth)
+
+ return true
+ })
+
+ return maxLenTagged
+}
+
+func (d *Debugger) updateTreeRelCols(colStartIdx int, steps []*am.Step) {
+ c := d.C
+ if c == nil {
+ return
+ }
+
+ // walk the tree only when scrolling steps
+ if c.CursorStep < 1 {
+ return
+ }
+
+ var relCols []RelCol
+ var closed bool
+
+ d.tree.GetRoot().WalkUnsafe(func(
+ node, parent *cview.TreeNode, depth int,
+ ) bool {
+ // skip the root
+ if parent == nil || !parentExpanded(node) {
+ return true
+ }
+
+ ref, ok := node.GetReference().(*nodeRef)
+ if !ok {
+ // TODO shouldnt happen
+ return true
+ }
+
+ var forcedCols []string
+ // debug
+ // d.Mach.Log(".")
+
+ if ref.stateName != "" {
+
+ // STATE NAME NODES
+ stateName := ref.stateName
+ for i := range steps {
+
+ if c.CursorStep == i {
+ break
+ }
+ step := steps[i]
+
+ if step.Type != am.StepRelation {
+ continue
+ }
+
+ isTarget := step.ToState == stateName && !ref.isRef
+ isSource := ref.isRef && step.ToState == stateName &&
+ ref.parentState == step.FromState
+
+ if isTarget || isSource {
+
+ colName := getRelColName(step)
+ relCols, closed = handleTreeCol(strconv.Itoa(depth), colName, relCols)
+
+ if closed {
+ // debug
+ // d.Mach.Log("close %s", colName)
+ forcedCols = append(forcedCols, colName)
+ }
+ //} else {
+ // debug
+ //d.Mach.Log("open %s", colName)
+ //}
+ }
+ }
+ }
+
+ // draw columns
+ nodeColStartIdx := colStartIdx - depth*treeIndent + treeIndent
+ nodeCols := strings.Repeat(" ", max(0,
+ nodeColStartIdx-node.VisibleLength()))
+
+ if len(relCols) > 0 {
+ nodeCols += "[white]"
+ }
+
+ active := 0
+ for _, col := range relCols {
+ forced := false
+ for _, forcedCol := range forcedCols {
+ if forcedCol == col.name {
+ forced = true
+ }
+ }
+ if !col.closed || forced {
+ // TODO color based on the map key
+ nodeCols += "|"
+ active++
+ } else {
+ // empty column
+ nodeCols += " "
+ }
+ }
+ // debug
+ // d.Mach.Log("cols: %d [%d] | s-idx: %d | len: %d", len(relCols),
+ // active, nodeColStartIdx, nodeVisibleLen(node))
+
+ node.SetText(node.GetText() + nodeCols)
+
+ // debug
+ // d.Mach.Log("%s%s", strings.Repeat("---", depth), node.GetText())
+
+ return true
+ })
+}
+
+func parentExpanded(node *cview.TreeNode) bool {
+ for node = node.GetParent(); node != nil; node = node.GetParent() {
+ if !node.IsExpanded() {
+ return false
+ }
+ }
+ return true
+}
+
+func handleTreeCol(source, name string, relCols []RelCol) ([]RelCol, bool) {
+ closed := false
+ for i, col := range relCols {
+ if col.name == name && col.source != source {
+ // close a column
+ relCols[i].closed = true
+ closed = true
+ }
+ }
+
+ if closed {
+ return relCols, true
+ }
+
+ // create a new column
+ relCols = append(relCols, RelCol{
+ colIndex: len(relCols),
+ name: name,
+ source: source,
+ })
+
+ return relCols, false
+}
+
+func getRelColName(step *am.Step) string {
+ return step.FromState + "-" + step.Data.(am.Relation).String() +
+ "-" + step.ToState
+}
+
+func (d *Debugger) handleExpanded(
+ node *cview.TreeNode, ref *nodeRef, c *Client,
+) {
+ if ref.isRef || ref.stateName == "" {
+ return
+ }
+
+ // expand when touched or expanded by the user
+ stepsMode := c.CursorStep > 0 || d.Mach.Is1(ss.TimelineStepsFocused)
+ node.SetExpanded(false)
+ if (ref.expanded && !stepsMode) || (ref.touched && stepsMode) {
+ node.SetExpanded(true)
+ }
+}
+
+func (d *Debugger) buildStatesTree() {
+ msg := d.C.MsgStruct
+ d.treeRoot.SetText(msg.ID)
+ d.treeRoot.ClearChildren()
+ for _, name := range msg.StatesIndex {
+ d.addState(name)
+ }
+ d.treeRoot.CollapseAll()
+ d.treeRoot.Expand()
+}
+
+func (d *Debugger) addState(name string) {
+ c := d.C
+ if c == nil {
+ return
+ }
+
+ state := c.MsgStruct.States[name]
+ stateNode := cview.NewTreeNode(name + " (0)")
+ stateNode.SetSelectable(true)
+ stateNode.SetReference(&nodeRef{stateName: name})
+ d.treeRoot.AddChild(stateNode)
+ stateNode.SetColor(colorInactive)
+
+ // labels
+ labels := ""
+ if state.Auto {
+ labels += "auto"
+ }
+
+ if state.Multi {
+ if labels != "" {
+ labels += " "
+ }
+ labels += "multi"
+ }
+
+ if labels != "" {
+ labelNode := cview.NewTreeNode(labels)
+ labelNode.SetSelectable(false)
+ labelNode.SetReference(&nodeRef{
+ isProp: true,
+ propLabel: labels,
+ })
+ stateNode.AddChild(labelNode)
+ }
+
+ // relations
+ addRelation(stateNode, name, am.RelationAdd, state.Add)
+ addRelation(stateNode, name, am.RelationRequire, state.Require)
+ addRelation(stateNode, name, am.RelationRemove, state.Remove)
+ addRelation(stateNode, name, am.RelationAfter, state.After)
+}
+
+// sortTree requires updateTree called before
+func (d *Debugger) sortTree() {
+ // sort state names in the tree with touched ones first
+ nodes := d.treeRoot.GetChildren()
+ slices.SortStableFunc(nodes, func(a, b *cview.TreeNode) int {
+ // sort by touched
+ refA := a.GetReference().(*nodeRef)
+ refB := b.GetReference().(*nodeRef)
+
+ if refA.touched && !refB.touched {
+ return -1
+ } else if !refA.touched && refB.touched {
+ return 1
+ }
+
+ // sort by machine order
+ idxA := slices.Index(d.C.MsgStruct.StatesIndex, refA.stateName)
+ idxB := slices.Index(d.C.MsgStruct.StatesIndex, refB.stateName)
+
+ if idxA < idxB {
+ return -1
+ } else {
+ return 1
+ }
+ })
+
+ d.treeRoot.SetChildren(nodes)
+}
+
+func addRelation(
+ stateNode *cview.TreeNode, name string, rel am.Relation, relations []string,
+) {
+ if len(relations) <= 0 {
+ return
+ }
+ relNode := cview.NewTreeNode(capitalizeFirst(rel.String()))
+ relNode.SetSelectable(true)
+ relNode.SetReference(&nodeRef{
+ isRel: true,
+ rel: rel,
+ parentState: name,
+ })
+
+ for _, relState := range relations {
+ stateNode := cview.NewTreeNode(relState)
+ stateNode.SetReference(&nodeRef{
+ isRef: true,
+ stateName: relState,
+ parentState: name,
+ })
+ relNode.AddChild(stateNode)
+ }
+
+ stateNode.AddChild(relNode)
+}
+
+type RelCol struct {
+ name string
+ // columns has been closed
+ closed bool
+ // column index
+ colIndex int
+ source string
+}
+
+func capitalizeFirst(s string) string {
+ if len(s) == 0 {
+ return s
+ }
+ return strings.ToUpper(string(s[0])) + s[1:]
+}
diff --git a/tools/generator/generator.go b/tools/generator/generator.go
new file mode 100644
index 0000000..bbeb651
--- /dev/null
+++ b/tools/generator/generator.go
@@ -0,0 +1,71 @@
+package generator
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/lithammer/dedent"
+)
+
+func GenStatesFile(states []string) string {
+ sStruct := ""
+ sDefs := ""
+ sNames := ""
+
+ for _, state := range states {
+ if state == "" {
+ continue
+ }
+
+ // capitalize the 1st letter, if not already
+ if state[0] >= 'a' && state[0] <= 'z' {
+ state = string(state[0]-32) + state[1:]
+ }
+
+ sStruct += "\t" + state + ": {},\n"
+ sDefs += "\t" + state + " = \"" + state + "\"\n"
+ sNames += "\t" + state + ",\n"
+ }
+
+ // TODO linebreaks
+ sNames = strings.Trim(sNames, ",")
+ sStruct = strings.Trim(sStruct, "\n")
+ sDefs = strings.Trim(sDefs, "\n")
+
+ return fmt.Sprintf(dedent.Dedent(strings.Trim(`
+ package states
+
+ import am "github.com/pancsta/asyncmachine-go/pkg/machine"
+
+ // S is a type alias for a list of state names.
+ type S = am.S
+
+ // States map defines relations and properties of states.
+ var States = am.Struct{
+ %s
+ }
+
+ // Groups of mutually exclusive states.
+
+ //var (
+ // GroupPlaying = S{Playing, Paused}
+ //)
+
+ //#region boilerplate defs
+
+ // Names of all the states (pkg enum).
+
+ const (
+ %s
+ )
+
+ // Names is an ordered list of all the state names.
+ var Names = S{
+ am.Exception,
+ %s
+ }
+
+ //#endregion
+
+ `, "\n")), sStruct, sDefs, sNames)
+}