diff --git a/README.md b/README.md index 35e375a3..e9bdc7ba 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,13 @@
-[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](LICENSE.md) [![Build](https://github.com/UIBK-DPS-DC/Cirrina/actions/workflows/build.yml/badge.svg?event=push)](https://github.com/UIBK-DPS-DC/Cirrina/actions/workflows/build.yml?event=push) [![Docker Image Version (tag)](https://img.shields.io/docker/v/collaborativestatemachines/cirrina/unstable?color=red)](https://hub.docker.com/repository/docker/collaborativestatemachines/cirrina/tags/unstable/) [![Docker Image Version (tag)](https://img.shields.io/docker/v/collaborativestatemachines/cirrina/stable?color=blue)](https://hub.docker.com/repository/docker/collaborativestatemachines/cirrina/tags/stable/) +[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](LICENSE.md) +[![Build](https://github.com/UIBK-DPS-DC/Cirrina/actions/workflows/build.yml/badge.svg?event=push)](https://github.com/UIBK-DPS-DC/Cirrina/actions/workflows/build.yml?event=push) +[![Docker Image Version (tag)](https://img.shields.io/docker/v/collaborativestatemachines/cirrina/stable?color=blue)](https://hub.docker.com/layers/collaborativestatemachines/cirrina/stable/) +[![Docker Image Version (tag)](https://img.shields.io/docker/v/collaborativestatemachines/cirrina/unstable?color=red)](https://hub.docker.com/layers/collaborativestatemachines/cirrina/unstable) Cirrina, a distributed Collaborative State Machines (CSM) runtime for the Cloud-Edge-IoT continuum. -Collaborative State Machines is a state machine-based programming model for the Cloud-Edge-IoT +Collaborative State Machines is a state-machine-based programming model for the Cloud-Edge-IoT continuum. For more information, please visit the [Project Website](https://collaborativestatemachines.github.io/). @@ -17,19 +20,3 @@ the [Distributed and Parallel Systems Group of the University Innsbruck](https://dps.uibk.ac.at/). For contributions, please see the [Contribution Guidelines](CONTRIBUTING.md). - -To deploy Cirrina using Kubernetes, see the [Helm Chart](charts/cirrina). - -## Citing - -``` -@misc{etheredge2025collaborativestatemachinesbetter, - title={Collaborative State Machines: A Better Programming Model for the Cloud-Edge-IoT Continuum}, - author={Marlon Etheredge and Thomas Fahringer and Felix Erlacher and Elias Kohler and Stefan Pedratscher and Juan Aznar-Poveda and Nishant Saurabh and Adrien Lebre}, - year={2025}, - eprint={2507.21685}, - archivePrefix={arXiv}, - primaryClass={cs.DC}, - url={https://arxiv.org/abs/2507.21685}, -} -``` diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..7f3820c6 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,15 @@ +# Cirrina Examples + +This directory contains a collection of examples. + +To run an example on multiple isolated resources, we recommend +using [Vagrant](https://developer.hashicorp.com/vagrant). + +To execute the [helloWorld](tutorial/helloWorld.pkl) example, run: + +``` +./cirrina \ + RUN=one,two \ + MAIN_URI=file:///examples/tutorial/helloWorld.pkl \ + ETCD_CONTEXT_URL=http://localhost:2379 +``` \ No newline at end of file diff --git a/examples/concurrency/big.pkl b/examples/concurrency/big.pkl new file mode 100644 index 00000000..869e4aeb --- /dev/null +++ b/examples/concurrency/big.pkl @@ -0,0 +1,116 @@ +amends + "https://raw.githubusercontent.com/CollaborativeStateMachines/Cirrina/refs/heads/develop/src/main/resources/pkl/csm/csml.pkl" + +local n = 12 + +collaborativeStateMachine { + stateMachines { + ["big"] { + transient { ["count"] = "0" } + states { + ["register"] = new Initial { + entry { new Emit { event { topic = "register" }; target = "'sink'" } } + on { + ["initial"] = new Transition { + to = "run" + } + } + } + ["run"] = new State { + entry { + new Eval { expression = "++count" } + new Ctr { counter = "big.pings" } + new Emit { + event { + topic = "ping" + data { ["sender"] = "id" } + } + target = "std:takeRandom(peers).toString()" + } + } + on { + ["ping"] { + yields { + new Emit { + event { + topic = "pong" + data { ["sender"] = "id" } + } + target = "$sender.toString()" + } + } + } + ["pong"] { + yields { + new Match { + cases { + new Case { + of = "count % 1000 == 0" + yields { + new Emit { event { topic = "report" } } + } + } + } + } + new Eval { expression = "++count" } + new Ctr { counter = "big.pings" } + new Emit { + event { + topic = "ping" + data { ["sender"] = "id" } + } + target = "std:takeRandom(peers).toString()" + } + } + } + } + } + } + } + ["sink"] = new StateMachine { + transient { + ["total"] = "0" + ["registered"] = "0" + } + states { + ["wait"] = new Initial { + on { + ["register"] = new Transition { + yields { + new Eval { expression = "++registered" } + new Match { + cases { + new Case { + of = "registered == 12" + yields { + new Emit { event { topic = "initial" } } + } + } + } + } + } + } + ["report"] { + yields { + new Eval { expression = "total += 1000" } + new Ctr { counter = "sink.total"; by = 1000 } + } + } + } + } + } + } + } +} +instances { + ["sink"] { stateMachineName = "sink" } + for (i in IntSeq(0, n - 1)) { + ["\(i)"] { + stateMachineName = "big" + data { + ["id"] = "\(i)" + ["peers"] = "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] - [\(i)]" + } + } + } +} diff --git a/examples/concurrency/chameneos.pkl b/examples/concurrency/chameneos.pkl new file mode 100644 index 00000000..d4a5e34b --- /dev/null +++ b/examples/concurrency/chameneos.pkl @@ -0,0 +1,100 @@ +amends "https://raw.githubusercontent.com/CollaborativeStateMachines/Cirrina/refs/heads/develop/src/main/resources/pkl/csm/csml.pkl" + +local n = 12 + +collaborativeStateMachine { + stateMachines { + ["chameneos"] { + transient { + ["color"] = "std:takeRandom([1,2,3,...])" + } + states { + ["request"] = new Initial { + entry { + new Emit { + event { + topic = "requesting" + data { ["id"] = "id"; ["color"] = "color" } + } + target = "'mall'" + } + } + on { + ["matchMade"] = new Transition { + to = "request" + yields { + new Eval { expression = "color = (function (x,y) { if (x == y) {x} else {x ^ y} })(color, $color)" } + new Emit { + event { + topic = "change" + data { ["color"] = "color" } + } + target = "$partner.toString()" + } + } + } + ["change"] = new Transition { + to = "request" + yields { + new Eval { expression = "color = $color" } + } + } + } + } + } + } + ["mall"] { + transient { + ["waiting"] = "[...]" + ["count"] = "0" + } + states { + ["wait"] = new Initial { + on { + ["requesting"] = new Transition { + yields { + new Match { + cases { + new Case { + of = "waiting.isEmpty()" + yields { new Eval { expression = "waiting += [$id]" } } + } + new Case { + of = "!waiting.isEmpty()" + yields { + new Eval { expression = "++count" } + new Ctr { counter = "mall.meetings" } + new Emit { + event { + topic = "matchMade" + data { + ["partner"] = "$id" + ["color"] = "$color" + } + } + target = "waiting.get(0).toString()" + } + new Eval { expression = "waiting -= [waiting.get(0)]" } + } + } + } + + } + } + } + } + } + } + } + } +} + +instances { + ["mall"] { stateMachineName = "mall" } + for (i in IntSeq(0, n - 1)) { + ["\(i)"] { + stateMachineName = "chameneos" + data { ["id"] = "\(i)" } + } + } +} diff --git a/examples/concurrency/cigaretteSmokers.pkl b/examples/concurrency/cigaretteSmokers.pkl new file mode 100644 index 00000000..da9414ad --- /dev/null +++ b/examples/concurrency/cigaretteSmokers.pkl @@ -0,0 +1,67 @@ +amends + "https://raw.githubusercontent.com/CollaborativeStateMachines/Cirrina/refs/heads/develop/src/main/resources/pkl/csm/csml.pkl" + +local n = 3 + +collaborativeStateMachine { + stateMachines { + ["arbiter"] { + transient { + ["ingredients"] = "[0, 1, 2, ...]" + ["count"] = "0" + } + states { + ["run"] = new Initial { + entry { + new Eval { expression = "++count" } + new Ctr { counter = "arbiter.rounds" } + new Emit { + event { + topic = "provided" + data { ["ingredients"] = "ingredients - [std:takeRandom(ingredients)]" } + } + } + } + on { + ["finish"] = new Transition { to = "run" } + } + } + } + } + ["smoker"] { + states { + ["wait"] = new Initial { + on { + ["provided"] = new Transition { + to = "smoking" + provided = "!($ingredients.contains(id))" + } + } + } + ["smoking"] = new State { + after { + ["wait"] = new Timeout { + delay = "std:randomAround(10,2)" + triggers = new Emit { event = new Internal { topic = "continue" } } + } + } + on { + ["continue"] = new Transition { + to = "wait" + yields { new Emit { event { topic = "finish" }; target = "'arbiter'" } } + } + } + } + } + } + } +} +instances { + ["arbiter"] { stateMachineName = "arbiter" } + for (i in IntSeq(0, n - 1)) { + ["\(i)"] { + stateMachineName = "smoker" + data { ["id"] = "\(i)" } + } + } +} diff --git a/examples/concurrency/count.pkl b/examples/concurrency/count.pkl new file mode 100644 index 00000000..558dffbd --- /dev/null +++ b/examples/concurrency/count.pkl @@ -0,0 +1,50 @@ +amends + "https://raw.githubusercontent.com/CollaborativeStateMachines/Cirrina/refs/heads/develop/src/main/resources/pkl/csm/csml.pkl" + +collaborativeStateMachine { + stateMachines { + ["producer"] { + states { + ["init"] = new Initial { + entry { + new Emit { event = new Internal { topic = "send" } } + } + on { ["send"] = new Transition { to = "send" } } + } + ["send"] = new State { + after { + ["send"] = new Timeout { + delay = "std:randomAround(10,2)" + triggers = new Emit { event = new Internal { topic = "send" } } + } + } + on { + ["send"] = new Transition { + to = "init" + yields { new Emit { event { topic = "increment" } } } + } + } + } + } + } + ["counter"] { + transient { ["count"] = "0" } + states { + ["count"] = new Initial { + on { + ["increment"] = new Transition { + yields { + new Eval { expression = "++count" } + new Ctr { counter = "counter.count" } + } + } + } + } + } + } + } +} +instances { + ["counter"] { stateMachineName = "counter" } + ["producer"] { stateMachineName = "producer" } +} diff --git a/examples/concurrency/diningPhilosophers.pkl b/examples/concurrency/diningPhilosophers.pkl new file mode 100644 index 00000000..c6fd9877 --- /dev/null +++ b/examples/concurrency/diningPhilosophers.pkl @@ -0,0 +1,124 @@ +amends + "https://raw.githubusercontent.com/CollaborativeStateMachines/Cirrina/refs/heads/develop/src/main/resources/pkl/csm/csml.pkl" + +local n = 6 + +collaborativeStateMachine { + stateMachines { + ["arbitrator"] { + transient { + ["forks"] = "std:repeat(false, \(n))" + ["waiting"] = "std:repeat(false, \(n))" + } + states { + ["serving"] = new Initial { + static { ["count"] = "0" } + on { + ["hungry"] { + yields { + new Match { + cases { + new Case { + of = "!(forks[$id] || forks[($id + 1) % 6])" + yields { + new Eval { expression = "forks[$id] = forks[($id + 1) % 6] = true" } + new Emit { event { topic = "acquire" }; target = "$id.toString()" } + } + } + } + default = new Eval { expression = "waiting[$id] = true" } + } + } + } + ["release"] { + yields { + new Eval { expression = "forks[$id] = forks[($id + 1) % 6] = false" } + new Emit { + event = new Internal { topic = "test"; data { ["nid"] = "($id + 5) % 6" } } + } + new Emit { + event = new Internal { topic = "test"; data { ["nid"] = "($id + 1) % 6" } } + } + } + } + ["test"] { + yields { + new Match { + cases { + new Case { + of = "waiting[$nid] && !(forks[$nid] || forks[($nid + 1) % 6])" + yields { + new Eval { + expression = + "waiting[$nid] = false; forks[$nid] = forks[($nid + 1) % 6] = true" + } + new Emit { event { topic = "acquire" }; target = "$nid.toString()" } + } + } + } + } + } + } + } + } + } + } + ["philosopher"] { + states { + ["hungry"] = new Initial { + entry { + new Emit { event { topic = "hungry"; data { ["id"] = "id" } }; target = "'arbitrator'" } + } + on { + ["acquire"] { to = "eating" } + } + } + ["eating"] { + static { ["meals"] = "0" } + after { + ["eating"] = new Timeout { + delay = "std:randomAround(10,2)" + triggers = new Emit { event = new Internal { topic = "ate" } } + } + } + on { + ["ate"] { + to = "thinking" + yields { + new Eval { expression = "++meals" } + new Ctr { counter = "philosopher.meals" } + new Emit { + event { topic = "release"; data { ["id"] = "id" } } + target = "'arbitrator'" + } + } + } + } + } + ["thinking"] { + after { + ["thinking"] = new Timeout { + delay = "std:randomAround(10,2)" + triggers = new Emit { event = new Internal { topic = "thought" } } + } + } + on { + ["thought"] { + to = "hungry" + } + } + } + } + } + } +} + +instances { + ["arbitrator"] { stateMachineName = "arbitrator" } + for (i in IntSeq(0, n - 1)) { + ["\(i)"] { + stateMachineName = "philosopher" + data { ["id"] = "\(i)" } + } + } +} diff --git a/examples/concurrency/dynamicPhilosophers.pkl b/examples/concurrency/dynamicPhilosophers.pkl new file mode 100644 index 00000000..dd51893d --- /dev/null +++ b/examples/concurrency/dynamicPhilosophers.pkl @@ -0,0 +1,322 @@ +amends +"https://raw.githubusercontent.com/CollaborativeStateMachines/Cirrina/refs/heads/develop/src/main/resources/pkl/csm/csml.pkl" + +local n = 12 + +collaborativeStateMachine { + stateMachines { + ["philosopher"] { + states { + ["hungry"] = new Initial { + entry { + new Emit { event = new Internal { topic = "evaluateRequests" } } + } + on { + ["evaluateRequests"] { + yields { + new Match { + cases { + new Case { + of = "leftNeighbor != 'none' && !hasLeftFork && !leftRequested" + yields { + new Eval { expression = "leftRequested = true" } + new Emit { event { topic = "requestRightFork" }; target = "leftNeighbor" } + } + } + new Case { + of = "rightNeighbor != 'none' && !hasRightFork && !rightRequested" + yields { + new Eval { expression = "rightRequested = true" } + new Emit { event { topic = "requestLeftFork" }; target = "rightNeighbor" } + } + } + } + } + new Emit { event = new Internal { topic = "tryEat" } } + } + } + + ["tryEat"] { + to = "eating" + provided = "(leftNeighbor == 'none' || hasLeftFork) && (rightNeighbor == 'none' || hasRightFork) && !(leftNeighbor == 'none' && rightNeighbor == 'none')" + } + + ["giveLeftFork"] { + yields { + new Eval { expression = "hasLeftFork = true; leftForkDirty = false; leftRequested = false" } + new Emit { event = new Internal { topic = "tryEat" } } + } + } + ["giveRightFork"] { + yields { + new Eval { expression = "hasRightFork = true; rightForkDirty = false; rightRequested = false" } + new Emit { event = new Internal { topic = "tryEat" } } + } + } + + ["requestLeftFork"] { + yields { + new Match { + cases { + new Case { + of = "hasLeftFork && leftForkDirty" + yields { + new Eval { expression = "hasLeftFork = false; leftPending = false" } + new Emit { event { topic = "giveRightFork" }; target = "leftNeighbor" } + new Emit { event = new Internal { topic = "evaluateRequests" } } + } + } + new Case { + of = "!(hasLeftFork && leftForkDirty)" + yields { + new Eval { expression = "leftPending = true" } + } + } + } + } + } + } + + ["requestRightFork"] { + yields { + new Match { + cases { + new Case { + of = "hasRightFork && rightForkDirty" + yields { + new Eval { expression = "hasRightFork = false; rightPending = false" } + new Emit { event { topic = "giveLeftFork" }; target = "rightNeighbor" } + new Emit { event = new Internal { topic = "evaluateRequests" } } + } + } + new Case { + of = "!(hasRightFork && rightForkDirty)" + yields { + new Eval { expression = "rightPending = true" } + } + } + } + } + } + } + + ["join"] { + yields { + new Eval { expression = "rightNeighbor = $id; hasRightFork = true; rightForkDirty = true" } + new Match { + cases { + new Case { + of = "rightPending" + yields { + new Eval { expression = "hasRightFork = false; rightPending = false" } + new Emit { event { topic = "giveLeftFork" }; target = "rightNeighbor" } + } + } + } + } + new Emit { event = new Internal { topic = "evaluateRequests" } } + } + } + } + } + + ["eating"] { + static { ["meals"] = "0" } + after { + ["eating"] = new Timeout { + delay = "std:randomAround(10,2)" + triggers = new Emit { event = new Internal { topic = "ate" } } + } + } + on { + ["ate"] { + to = "thinking" + yields { + new Eval { expression = "++meals; leftForkDirty = true; rightForkDirty = true" } + new Ctr { counter = "philosopher.meals"; tags { ["id"] = "id.toString()" } } + new Match { + cases { + new Case { + of = "leftPending && hasLeftFork" + yields { + new Eval { expression = "hasLeftFork = false; leftPending = false" } + new Emit { event { topic = "giveRightFork" }; target = "leftNeighbor" } + } + } + } + } + new Match { + cases { + new Case { + of = "rightPending && hasRightFork" + yields { + new Eval { expression = "hasRightFork = false; rightPending = false" } + new Emit { event { topic = "giveLeftFork" }; target = "rightNeighbor" } + } + } + } + } + } + } + ["requestLeftFork"] { yields { new Eval { expression = "leftPending = true" } } } + ["requestRightFork"] { yields { new Eval { expression = "rightPending = true" } } } + ["join"] { + yields { + new Eval { expression = "rightNeighbor = $id; hasRightFork = true; rightForkDirty = true" } + } + } + } + } + + ["thinking"] { + after { + ["thinking"] = new Timeout { + delay = "std:randomAround(10,2)" + triggers = new Emit { event = new Internal { topic = "thought" } } + } + } + on { + ["thought"] { to = "hungry" } + ["requestLeftFork"] { + yields { + new Match { + cases { + new Case { + of = "hasLeftFork" + yields { + new Eval { expression = "hasLeftFork = false; leftPending = false" } + new Emit { event { topic = "giveRightFork" }; target = "leftNeighbor" } + } + } + new Case { + of = "!hasLeftFork" + yields { + new Eval { expression = "leftPending = true" } + } + } + } + } + } + } + ["requestRightFork"] { + yields { + new Match { + cases { + new Case { + of = "hasRightFork" + yields { + new Eval { expression = "hasRightFork = false; rightPending = false" } + new Emit { event { topic = "giveLeftFork" }; target = "rightNeighbor" } + } + } + new Case { + of = "!hasRightFork" + yields { + new Eval { expression = "rightPending = true" } + } + } + } + } + } + } + ["join"] { + yields { + new Eval { expression = "rightNeighbor = $id; hasRightFork = true; rightForkDirty = true" } + new Match { + cases { + new Case { + of = "rightPending" + yields { + new Eval { expression = "hasRightFork = false; rightPending = false" } + new Emit { event { topic = "giveLeftFork" }; target = "rightNeighbor" } + } + } + } + } + } + } + ["giveLeftFork"] { yields { new Eval { expression = "hasLeftFork = true; leftForkDirty = false" } } } + ["giveRightFork"] { yields { new Eval { expression = "hasRightFork = true; rightForkDirty = false" } } } + } + } + } + } + + ["instantiator"] { + states { + ["wait"] = new Initial { + always { + new Transition { + to = "instantiate" + provided = "count == 0" + yields { new Eval { expression = "count = \(n)" } } + } + } + on { + ["nodeCreated"] { to = "wait"; yields { new Eval { expression = "--count" } } } + ["increment"] { yields { new Eval { expression = "lastInstantiated += \(n)" } } } + } + } + ["instantiate"] = new State { + after { + ["instantiate"] = new Timeout { + delay = "5000" + triggers = new Emit { event = new Internal { topic = "instantiate" } } + } + } + on { + ["instantiate"] { + to = "wait" + yields { + new Instantiate { + instances { + [Pair("instantiated", "lastInstantiated.toString()")] = new Instance { + stateMachineName = "philosopher" + data { + ["id"] = "lastInstantiated" + ["leftNeighbor"] = "lastInstantiated == 0 ? 'none' : 'instantiated' + (lastInstantiated - 1).toString()" + ["rightNeighbor"] = "'none'" + ["hasLeftFork"] = "false" + ["hasRightFork"] = "false" + ["leftForkDirty"] = "true" + ["rightForkDirty"] = "true" + ["leftRequested"] = "false" + ["rightRequested"] = "false" + ["leftPending"] = "false" + ["rightPending"] = "false" + } + } + } + } + new Match { + cases { + new Case { + of = "lastInstantiated > 0" + yields { + new Emit { + event { topic = "join"; data { ["id"] = "'instantiated' + lastInstantiated.toString()" } } + target = "'instantiated' + (lastInstantiated - 1).toString()" + } + } + } + } + } + new Emit { event { topic = "nodeCreated" } } + new Emit { event = new Internal { topic = "increment" } } + } + } + } + } + } + } + } +} + +instances { + for (i in IntSeq(0, n - 1)) { + ["instantiator\(i)"] { + stateMachineName = "instantiator" + data { ["id"] = "\(i)"; ["count"] = "\(i)"; ["lastInstantiated"] = "\(i)" } + } + } +} diff --git a/examples/concurrency/pingPong.pkl b/examples/concurrency/pingPong.pkl new file mode 100644 index 00000000..f6ec5e27 --- /dev/null +++ b/examples/concurrency/pingPong.pkl @@ -0,0 +1,28 @@ +amends "https://raw.githubusercontent.com/CollaborativeStateMachines/Cirrina/refs/heads/develop/src/main/resources/pkl/csm/csml.pkl" + +collaborativeStateMachine { + stateMachines { + ["ping"] { + states { + ["ping"] = new Initial { + entry { + new Emit { event { topic = "ping" } } + new Ctr { counter = "ping.count" } + } + on { ["pong"] { to = "ping" } } + } + } + } + ["pong"] = new StateMachine { + states { + ["pong"] = new Initial { + on { ["ping"] { yields { new Emit { event { topic = "pong" } } } } } + } + } + } + } +} +instances { + ["ping"] { stateMachineName = "ping" } + ["pong"] { stateMachineName = "pong" } +} \ No newline at end of file diff --git a/examples/concurrency/sleepingBarber.pkl b/examples/concurrency/sleepingBarber.pkl new file mode 100644 index 00000000..a5695c77 --- /dev/null +++ b/examples/concurrency/sleepingBarber.pkl @@ -0,0 +1,147 @@ +amends +"https://raw.githubusercontent.com/CollaborativeStateMachines/Cirrina/refs/heads/develop/src/main/resources/pkl/csm/csml.pkl" + +local n = 12 + +collaborativeStateMachine { + stateMachines { + ["barber"] { + states { + ["sleeping"] = new Initial { + on { + ["sit"] = new Transition { + to = "cutting" + yields { new Emit { event { topic = "comeIn" }; target = "$customer.toString()" } } + } + } + } + ["cutting"] = new State { + after { + ["wait"] = new Timeout { + delay = "std:randomAround(10,2)" + triggers = new Emit { event = new Internal { topic = "continue" } } + } + } + on { + ["continue"] = new Transition { + to = "sleeping" + yields { + new Emit { event { topic = "done" }; target = "$customer.toString()" } + new Emit { event { topic = "ready" }; target = "'waitingRoom'" } + } + } + } + } + } + } + ["customer"] { + transient { ["counter"] = "0" } + states { + ["request"] = new Initial { + entry { + new Emit { + event { topic = "enter"; data { ["customer"] = "id" } } + target = "'waitingRoom'" + } + } + on { + ["full"] = new Transition { to = "backoff" } + ["comeIn"] = new Transition { to = "haircut" } + } + } + ["backoff"] = new State { + after { + ["retry"] = new Timeout { + delay = "std:randomAround(10,2)" + triggers = new Emit { event = new Internal { topic = "retry" } } + } + } + on { + ["retry"] = new Transition { to = "request" } + } + } + ["haircut"] = new State { + entry { + new Eval { expression = "++counter" } + new Ctr { counter = "customer.haircuts" } + } + on { + ["done"] = new Transition { to = "request" } + } + } + } + } + ["waitingRoom"] { + transient { + ["waiting"] = "[...]" + ["isBarberSleeping"] = "true" + } + states { + ["process"] = new Initial { + on { + ["enter"] = new Transition { + yields { + new Match { + cases { + new Case { + of = "waiting.size() < 3" + yields { + new Eval { expression = "waiting.add($customer)" } + new Match { + cases { + new Case { + of = "isBarberSleeping" + yields { + new Eval { expression = "isBarberSleeping = false" } + new Emit { + event { topic = "sit"; data { ["customer"] = "waiting.get(0)" } } + target = "'barber'" + } + new Eval { expression = "waiting.remove(0)" } + } + } + } + } + } + } + } + default = new Emit { event { topic = "full" }; target = "$customer.toString()" } + } + } + } + ["ready"] = new Transition { + yields { + new Eval { expression = "isBarberSleeping = true" } + new Match { + cases { + new Case { + of = "!waiting.isEmpty()" + yields { + new Eval { expression = "isBarberSleeping = false" } + new Emit { + event { topic = "sit"; data { ["customer"] = "waiting.get(0)" } } + target = "'barber'" + } + new Eval { expression = "waiting.remove(0)" } + } + } + } + } + } + } + } + } + } + } + } +} +instances { + ["barber"] { stateMachineName = "barber" } + ["waitingRoom"] { stateMachineName = "waitingRoom" } + for (i in IntSeq(0, n - 1)) { + ["\(i)"] { + stateMachineName = "customer" + data { ["id"] = "\(i)" } + } + } +} diff --git a/examples/tutorial/helloWorld.pkl b/examples/tutorial/helloWorld.pkl new file mode 100644 index 00000000..2d28d4d3 --- /dev/null +++ b/examples/tutorial/helloWorld.pkl @@ -0,0 +1,58 @@ +amends + "https://raw.githubusercontent.com/CollaborativeStateMachines/Cirrina/refs/heads/develop/src/main/resources/pkl/csm/csml.pkl" + +collaborativeStateMachine { + stateMachines { + // The first state machine class has two states. + ["one"] { + states { + // Append to the global `message` variable and send an event to the second state machine. + ["a"] = new Initial { + entry { + new Eval { expression="message += 'Hello, '" } + new Emit { event { topic="advance" }; target="'two'" } + } + // Upon receiving an event back from the second state machine, progress to the next state. + on { + ["advance"] { + to = "b" + } + } + } + // Log the message and terminate the state machine. + ["b"] = new Terminal { + entry { + new Log { message="message" } + } + } + } + } + // The second state machine class has two states. + ["two"] { + states { + ["a"] = new Initial { + // Upon receiving an event from the first state machine, progress to the next state. + on { + ["advance"] { + to = "b" + } + } + } + // Append to the global `message` variable and send an event to the first state machine. + ["b"] = new Terminal { + entry { + new Eval { expression="message += 'World!'" } + new Emit { event { topic="advance" }; target="'one'" } + } + } + } + } + } + // Declare the global `message` variable. + persistent { ["message"] = "''" } +} +// Declare one instance per state machine class. +instances { + ["one"] { stateMachineName = "one" } + ["two"] { stateMachineName = "two" } +} \ No newline at end of file diff --git a/version.txt b/version.txt index 50aea0e7..e3a4f193 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2.1.0 \ No newline at end of file +2.2.0 \ No newline at end of file