New in 0.16.2

v0.16.3

Workflow

Workflow Implementation

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 can have a workflow dispatching 1000 child-workflows managing 1000 tasks each.

Good Practices

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,

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.

JAVA ONLY: 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.

Properties

Properties in workflows are saved along with the workflow state. Properties are especially useful in workflows where multiple methods are called, either sequentially or in parallel. They can represent business values updated by these methods. An example can be found in the introduction, illustrating a Loyalty Program.

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.HelloWorkflowImpl

@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.

Workflow Context

In some cases, it is useful to understand more about the context in which a workflow is executed.

The io.infinitic.workflows.Workflow class provides the following static properties:

NameTypeDescription
workflowIdStringUnique identifier of the workflow instance
workflowNameStringName of the workflow
methodIdStringUnique identifier of the method run
methodNameStringName of the method currently running
tagsSet<String>Tags provided when dispatching the workflow
metaMap<String, ByteArray>Metadata provided when dispatching the workflow

tags

The tags property of the workflow context is an immutable set of strings. Its value is defined when creating the stub before dispatching the workflow:

meta

The meta property of the workflow context is a immutable map of strings to arrays of bytes. Its value is defined when creating the stub before dispatching the workflow:

Service's Exceptions

As discussed in the service syntax documentation, it's recommended to handle non-technical failures through a status in the return value of services. Infinitic doesn't expect throws clauses in Service interfaces.

If a Service interface does contain throws clauses:

  • In Java: Add them to the workflow class or method, or use the @SneakyThrows annotation from Lombok.
  • In Kotlin: No additional action is required due to Kotlin's exception handling.

For more information on error handling in workflows, refer to the error handling documentation.

Previous
Logging