diff --git a/.gitignore b/.gitignore index 49f7c8d..f04e64e 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,7 @@ 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 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..d93a52f 100644 --- a/README.md +++ b/README.md @@ -1,179 +1,408 @@ # asyncmachine-go -`asyncmachine-go` is a minimal implementation of [AsyncMachine](https://github.com/TobiaszCudnik/asyncmachine) in +**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. +> **asyncmachine-go** is a general purpose state machine for managing complex asynchronous workflows in a safe and +> structured way. -```go -package main +![TUI Debugger](assets/am-dbg.gif) -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, }, - "FileDownloaded": { - Remove: am.S{"DownloadingFile"}, + MatrixView: {Remove: GroupViews}, + TreeMatrixView: {Remove: GroupViews}, + TailMode: { + Require: S{ClientSelected}, + Remove: GroupPlaying, }, - // ProcessFileActivity - "ProcessingFile": { + Playing: { + Require: S{ClientSelected}, + Remove: am.SMerge(GroupPlaying, S{LogUserScrolled}), + }, + 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 by default, 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 gives you:** states, events, thread-safety, logging, metrics, traces, debugger, history, flow constraints, scheduler + +> **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 + +## 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 +// 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}, +} +``` + +
+ +### [Temporal FileProcessing workflow](/examples/temporal-fileprocessing/fileprocessing.go) + +- [origin](https://github.com/temporalio/samples-go/blob/main/fileprocessing/) + +
+ +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{ + 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}, } ``` +
+ +### [AM as an Asynq worker](examples/asynq-fileprocessing/fileprocessing_task.go) + ## Documentation -- [API godoc](https://godoc.org/github.com/pancsta/asyncmachine-go/pkg/machine) -- [Manual](/docs/manual.md) +- [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) @@ -182,63 +411,196 @@ case <-mach.When(am.S{"FileUploaded"}, nil): - [Relations](/docs/manual.md#relations) - [Queue](/docs/manual.md#queue) -## TUI Debugger +## Tooling -`am-dbg` is a simple, yet effective tool to debug your machines, including: +### Debugger -- states with relations +![TUI Debugger](assets/am-dbg.png) + +`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 -- logs +- import / export +- matrix view -![TUI Debugger](assets/am-dbg.png) +See [tools/cmd/am-dbg](tools/cmd/am-dbg) for more info. -### Installing `am-dbg` +### Generator -[Download a release binary](https://github.com/pancsta/asyncmachine-go/releases/latest) or use `go install`: +`am-gen` will quickly bootstrap a typesafe states file for you. -`go install github.com/pancsta/asyncmachine-go/tools/am-dbg@latest` +`$ am-gen states-file Foo,Bar` -### Using `am-dbg` +
-Set up telemetry: +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) 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") -``` +## Integrations -## Changelog +### Open Telemetry -Latest release: `v0.3.1` + + + + Test duration chart + + +[`pkg/telemetry`](pkg/telemetry) 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 [`pkg/telemetry`](pkg/telemetry) for more info or [import a sample asset](assets/bench-jaeger-3h-10m.traces.json). + +### Prometheus -See [CHANELOG.md](/CHANGELOG.md) for details. + + + + Test duration chart + -## Status +[`pkg/telemetry/prometheus`](pkg/telemetry/prometheus) 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. -**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). +See [`pkg/telemetry/prometheus`](pkg/telemetry/prometheus) for more info. -## TODO +## 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 + + + + + Test duration chart + + +- **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 grafana dashboard + + +- **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`. +- **pubsub discovery** - eg `ps-17-disc` (10 states)
+ Discovery machine is a simple event loop with Multi states and a periodic refresh state. +- **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) +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). + +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..a8f653b --- /dev/null +++ b/config/.mdl_style.rb @@ -0,0 +1,10 @@ +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' 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..f2eba96 --- /dev/null +++ b/docs/cookbook.md @@ -0,0 +1,605 @@ +# Cookbook + +Cookbook for asyncmachine-go contains numerous snippets of common patterns. + +## ToC + + + +1. [ToC](#toc) +2. [Activation handler with negotiation](#activation-handler-with-negotiation) +3. [De-activation handler with negotiation](#de-activation-handler-with-negotiation) +4. [State to state handlers](#state-to-state-handlers) +5. [Debugging a machine](#debugging-a-machine) +6. [Simple logging](#simple-logging) +7. [Custom logging](#custom-logging) +8. [Minimal machine init](#minimal-machine-init) +9. [Common machine init](#common-machine-init) +10. [Wait for a state activation](#wait-for-a-state-activation) +11. [Wait for a state activation with argument values](#wait-for-a-state-activation-with-argument-values) +12. [Wait for a state de-activation](#wait-for-a-state-de-activation) +13. [Synchronous state (single)](#synchronous-state-single) +14. [Asynchronous state (double)](#asynchronous-state-double) +15. [Asynchronous boolean state (triple)](#asynchronous-boolean-state-triple) +16. [Full asynchronous boolean state (quadruple)](#full-asynchronous-boolean-state-quadruple) +17. [Input `Multi` state](#input-multi-state) +18. [Self removal state](#self-removal-state) +19. [State context](#state-context) +20. [Step context](#step-context) +21. [Dynamic handlers](#dynamic-handlers) +22. [Channel responses via arguments](#channel-responses-via-arguments) +23. [Equivalent of `select` write with `default`](#equivalent-of-select-write-with-default) +24. [Custom exception handler](#custom-exception-handler) +25. [Verify states at runtime](#verify-states-at-runtime) +26. [Typesafe states pkg template](#typesafe-states-pkg-template) +27. [State Mutex Groups](#state-mutex-groups) +28. [Transition-aware handler](#transition-aware-handler) +29. [Batch data into a single transition](#batch-data-into-a-single-transition) + 1. [Switch a state group](#switch-a-state-group) + 2. [DiffStates to navigate the flow](#diffstates-to-navigate-the-flow) + 3. [Pass data from a negotiation handler to the final handler](#pass-data-from-a-negotiation-handler-to-the-final-handler) + 4. [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() +``` + +## Dynamic handlers + +```go +events := []string{"FileDownloaded", am.EventQueueEnd} +ch := mach.DynHandler(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..f14e591 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 v0.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..b31525f 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 v0.5.11 h1:W/Z0OZfw+g9jIV8SJ0wUw9/Dy/KgVcSE+o+evxr7/jU= +github.com/pancsta/cview v0.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..c91956f --- /dev/null +++ b/scripts/dep-taskfile.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d \ No newline at end of file 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) +}