weekly update CW 11

35 views
Skip to first unread message

Roland Kuhn

unread,
Mar 17, 2023, 1:08:23 PM3/17/23
to Actyx users
Hi everyone!

It’s been a while since the last update and we’ve been busy 😊. The first I’ll mention is a wholly rewritten section of the documentation on querying and tagging with some guidance on short-lived versus long-lived entities — where previously we attempted a one-size-fits-all with Actyx Pond for both cases we now are moving towards a clearer distinction and better performance (more on that below).

For the second part, work is moving along nicely towards giving you control over event retention policies. To this end we’re establishing the concept of configurable event routing: using tag expressions (like you know from AQL) the Actyx node admin decides which stream an event goes into. These streams can be freely named in the node settings and configured to retain events up to a given age, count, or size. The basics are working in our lab, but in a few weeks we should have this feature ready for release. Event routing has been foreseen in the design of Actyx v2 from the beginning and it will play a more prominent role in the future, for example for configuring quality of service like low-latency machine coordination or high-latency batching of sensor inputs.

The third part concerns the Actyx Pond and its successor. While we’re not yet ready for public release I feel sufficiently confident this week to give you a sneak preview of what we call machines (which is short for state machines, but the name isn’t final yet — let me know if you like automata better). The formulation of these machines uses an API that is quite different from the Pond, even though you’ll find that the business logic will look the same (so don’t worry about having to rewrite everything!). We begin by declaring event types:

const Requested = Event.design('Requested').withPayload<{
  pickup: string
  destination: string
}>()

Then we bundle up all event types belonging together in a protocol. As in the previous weekly update we are using a taxi ride as our running example, where a passenger requests a ride and the taxis place bids for which one gets to perform the service.

const taxi = Protocol.make('taxiRide', [Requested, Bid, BidderID, Selected, ...])

Next we declare the various states in which our machine (or former Fish) could be, starting with the initial state:

const InitialP = protocol
  .designEmpty('InitialP')
  .command('request', [Requested], (context, params: { pickup: string; destination: string }) => [
    Requested.make(params),
  ])
  .finish()

You can see that the available commands this machine responds to are coupled to the individual states. With the Pond you have probably written code to check that a given command is only valid under certain conditions, with machines we lift this common pattern into the API. The `context` argument to the function that computes the events to be emitted facilitates access to the payload data stored in the state, but here we are looking at a state that doesn’t have parameters.

The missing piece now is what corresponds to `onEvent` in a Fish: the logic that describes how events affect the current state. Since we have our states nicely separated this will no longer be a giant switch statement, instead the new API allows you to describe each state transition in isolation:

InitialP.react([Requested, Bid, BidderID], AuctionP, (context, [requested, bid, bidderId]) => {
  const { pickup, destination } = requested
  return AuctionP.make({
    pickup,
    destination,
    bids: [
      {
        bidderID: bidderId.id,
        price: bid.price,
        time: new Date(bid.time),
      },
    ],
  })
})

This code describes that from the state `InitialP` we transition into state `AuctionP` if we get a sequence of three events: the ride request and the first pair of a taxi’s bid and bidderID events. In the Pond you’re forced to handle these events one by one, but since we’ve seen many use-cases where a fish would only react after receiving multiple events we have now built this into the API. The function provided as third argument to `.react()` again gets access to the current state via the `context` parameter and it gets the whole sequence of the three required events — the closure is only invoked once all of them have been received. Just as for `onEvent` the job of the closure is only and exactly to compute the next state, so here you’d move the business logic from one branch of a fish’s switch statement.

This is quite a big change, so why are we doing it?

With this API our library can fully see and understand the behaviour of your code as far as commands and events are concerned. This means that we can (and will) add features like
  • printing out a diagram describing what you have actually implemented so that you can discuss it with other people
  • checking that you have actually implemented the protocol that has been agreed upon
  • checking that the implemented protocol will play nicely between a set of machines that are supposed to work together
In other words, we will provide better tools for describing, testing, validating, and possibly even verifying your code — correctness by design.
Capturing the structure of your code also allows us to provide better APIs for running machines and reacting to their state changes. We’re moving away from the Pond model that recognises fishes by name and keeps them updated forever once you have asked for their state once. Instead, we offer a “machine runner” that takes care of a single machine for as long as you need it and offers a `.destroy()` method to clean it up.

const passenger = createMachineRunner(actyx, where, InitialP, undefined)
passenger.events.on('change', () => console.log(passenger.get()))
// ... and then later
passenger.destroy()

TypeScript has a nice language feature that makes it quite convenient to consume all states produced by a machine over time in an asynchronous fashion:

for await (const state of passenger) {
  if (state.is(InitialP)) {
    state.payload // now is properly typed for the initial state
  } else if (state.is(AuctionP)) {
    const bids = state.payload.bids
    if (bids.length > 2) {
      await state.commands.select(bids[1].id) // will emit a Selected event
    }
  } else ...
}

The interesting property of such a loop (and using `passenger` as an AsyncGenerator) is that the `.destroy()` call cannot be forgotten as it is triggered when the loop ends (e.g. by using `break`, `return`, or `throw`). The above example also shows how state matching is supported in our new API.

This email has gotten quite long, probably because I’m very enthusiastic about these developments. Just to be clear, Actyx Pond is not going away, and the new machine-runner library is based on the same old Actyx SDK using the well-known Actyx HTTP API endpoints you are using today. So you’ll be able to play with this once it is out and start using it at your own leisure.

With this I wish you all an excellent weekend,

Roland

--
Dr. Roland Kuhn
CTO — Actyx AG

Reply all
Reply to author
Forward
0 new messages