New version 0.13.0!

v0.13.0

Workflows

Workflows Syntax

Here is an example of workflow implementation, from our Hello World app:

As we can see above, a workflow is directly coded in plain java/kotlin - but the processing of this workflow is actually event-based, making Infinitic really scalable and error-resilient.

For more detailed explanations, please read under the hood of a event-driven workflow engine.

The abstract class io.infinitic.workflows.Workflow exposes a set of useful functions to:

Constraints

A workflow class must

  • extend io.infinitic.workflows.Workflow
  • be public and have an empty constructor
  • have serializable parameters and return value for its methods

A workflow class must be deterministic and without side effects. As a consequence, the following actions must not be used in workflows (but are perfectly fine in tasks):

  • multi-threading
  • performing database requests
  • performing any file operation
  • performing API calls
  • using environment variables*
  • using current date*
  • using random values*

*can be used in a workflow as inline tasks.

the history of a workflow should not grow indefinitely, so we should avoid having more than a few thousand tasks in a workflow. If we need more, we should consider using child workflows to distribute our work.

For example, to manage 1 million tasks, we may have a workflow dispatching 1000 child-workflows managing 1000 tasks each)

Recommendations

For easier versioning of workflows, we recommend that:

Each workflow should be given a simple name through the @Name annotation

Public methods should have:

  • one parameter of a dedicated type object
  • a return value of a dedicated type object

For example,

Properties

In some cases, we want to know more about the context of the execution of a workflow. An instance contains the following properties:

NameTypeDescription
tagsSet<String>tags of this workflow instance
workflowIdStringid of this workflow instance
workflowNameStringname of the workflow
methodIdStringid of this method run
methodNameStringname of the method currently running

Dispatch a task

Workflows only need to know the interface of remote services to be able to use them.

By using the newService function on the service interface, we create a stub that behaves syntactically as an instance of the remote service, but actually sends a message to Pulsar that will trigger the remote execution of the service.

Each call of a method will dispatch a new task. For example:

newService stubs can to be defined only once. We can use it multiple times to dispatch multiple new tasks.

If the return type of the task is void, we need to use dispatchVoid function instead of dispatch.

We can also add tags to this stub. If we do that, every task dispatched with it will be tagged as well. It's very useful to target later this instance by tag:

We can define global timeout for tasks at workflow level by adding @Timeout annotations to the Servide interface. It's also possible to extend the WithTimeoutinterface.

A global timeout represents the maximal duration of the task dispatched by workflows (including retries and transportation) before a timeout is thrown at workflow level for this task.

Defining global timeouts can be useful to ensure that a workflow is never stuck.

Dispatch a child-workflow

By using the newWorkflow function on a workflow interface, we create a stub that behaves syntactically as an instance of the workflow but sends a message to Pulsar that will trigger the remote execution of the workflow.

Each call of a method will dispatch a new child-workflow. For example:

newWorkflow stubs can be defined only once. We can use it multiple times to dispatch multiple new workflows.

If the return type of the method used is void, we need to use dispatchVoid function instead of dispatch.

We can also add tags to this stub. If we do that, every workflow dispatched with it will be tagged as well. It's very useful to target later this instance by tag:

We can define global timeout for child-workflows at workflow level by adding @Timeout annotations to the child Workflow interface. It's also possible to extend the WithTimeoutinterface.

A global timeout represents the maximal duration of the child workflow before a timeout is thrown at workflow level for it.

Defining global timeouts can be useful to ensure that a workflow is never stuck.

Inline task

As described here, any non-deterministic instructions, or instructions with side-effect, should be in tasks, not in workflows. For very simple instructions, it can be frustrating to write such simple tasks. For those cases, we can use inline tasks:

If the return type of the lambda describing the inline task is void, we need to use inlineVoid function instead of inline.

Receive signal

Workflow can receive signals from "outside". Signals are typed and sent through "channels". The workflow interface must have a getter method returning a SendChannel<Type>. For example:

Workflows implement channels with the channel function:

Channels can be of any serializable type.

Per default, a signal sent to a running workflow is discarded. Before a workflow can receive a signal, it must first declare that it is waiting for it using the receive method on the channel.

Manage time

Time can be managed using the timer function. A call to the timer function creates a Deferred<Instant> that will be completed at the given time:

We can also target a specific Instant:

When a workflow is waiting, no resources are consumed. Internally, a delayed Pulsar message is sent to wake up the workflow when the time is right.

Interacting with other workflows

It's possible to interact with another running workflow from a workflow. To do so, we create the stub of a running workflow from its id:

Alternatively, we can create a stub targeting all running workflow having a given tag:

Using this stub, we can:

  • send a signal to it
  • start another method in parallel
  • get current properties

Sending a signal to another workflow

Once we have the stub of a running workflow, we can easily send a typed signal to it:

If we target a running workflow by tag, the event will be sent to all running workflows with this tag:

Starting another method for a running workflow

When we use the stub of a running workflow to start a method, we actually create another execution running in parallel to the main one.

Get or set current properties of another workflow

When multiple methods (of the same workflow instance) are running in parallel, they share the instance properties.

For example, dispatching getters or setters of a workflow is a way to get or set properties in another workflow. In the example below, we can use the getter/setter methods of points property from another workflow. Also, the bonus method lets us add a bonus to the current value of points.

@Name annotation

A workflow instance is internally described by both its full java name (package included) and the name of the method called.

We may want to avoid coupling this name with the underlying implementation, for example, if we want to rename the class or method, or if we mix programming languages.

Infinitic provides a @Name annotation that let us declare explicitly the names that Infinitic should use internally. For example:

When using this annotation, the Service name setting in Workflow workers configuration file should be the one provided by the annotation:

workflows:
  - name: HelloWorkflow
    class: hello.world.workflows.HelloWorldWorkflowImpl

@Ignore annotation

At each step of the execution of a workflow, its properties are automatically serialized and stored. Those properties are part of the state of the workflow.

The @Ignore (io.infinitic.annotations.Ignore) annotation lets us tag other properties that are not part of the workflow state and should not be serialized during the workflow execution.

Previous
Workflow Workers