Skip to content

Blog

Businessworkflows met Temporal

  • Home
  • Blog
  • Businessworkflows met Temporal

Workflows, orchestraties, saga’s… Verschillende begrippen voor ongeveer hetzelfde: business processen waarin verschillende services, systemen of applicaties worden aangeroepen en gecoördineerd. Dergelijke processen in goede banen leiden brengt voor een ontwikkelaar de nodige complexiteit met zich mee. Naast dat je business logic in orde moet zijn, moet je onder andere rekening houden met schaalbaarheid, state persistence, timers, heartbeat monitoring en foutafhandeling- en herstel. Gelukkig zijn er frameworks die deze complexiteit grotendeels van je overnemen. Eén van deze frameworks is Temporal.

In deze blog probeer ik om je een korte schets van dit framework, wat ik momenteel voor één van onze opdrachtgevers gebruik, te geven.

 

Wat is Temporal?

Temporal handelt de meest voorkomende uitdagingen van het schrijven van orchestraties voor je af. Waardoor jij als ontwikkelaar je volledig kunt storten op de business logic. Want daar worden de meeste developers (ik in ieder geval!) gelukkig van. Orchestraties worden binnen Temporal ‘Workflows’ genoemd. En het mooie is… deze workflows stel je gewoon samen door code te schrijven. Dus geen geworstel met het tekenen van BPMN-plaatjes, of samenstellen van YAML-configuratie. Gewoon code. De ontwikkelaars bieden meerdere SDK’s aan: onder andere in Java en Go.

Twee belangrijke begrippen binnen het Temporal framework zijn Workflows en Activities.

Workflows

Een workflow is een weergave van alle stappen die binnen een orchestratie moeten worden uitgevoerd. Belangrijk om te onthouden is dat code binnen workflows geen side effects mag bevatten. Binnen de workflows wordt ook de state van een orchestratie bijgehouden. Dit kan door simpelweg een waarde aan class fields toe te kennen.

Activities

Activities bevatten juist wél side effects en bewerkingen. Activities worden binnen workflows gebruikt om acties of bewerkingen uit te voeren.

 

Hoe werkt het?

Nadat een workflow is gestart, wordt zijn status bijgehouden op de Temporal server. Elke stap die binnen een workflow wordt gedaan, wordt hier opgeslagen, net als de activities die zijn gestart en welke return values hieruit zijn teruggekomen. Hierdoor weet Temporal altijd welke stappen er al zijn uitgevoerd en wordt voorkomen dat operaties meermaals worden uitgevoerd. Mocht er ergens in de workflow een fout optreden, dan weet Temporal precies waar in de workflow hij was gebleven en kan hij de workflow vanaf dat punt weer voortzetten. De taken binnen activities en workflows worden uitgevoerd door Workers. Deze Workers pollen Temporal voor nieuwe taken en koppelen het resultaat vervolgens aan Temporal terug. Er kunnen talloze workers actief zijn, op meerdere machines. Dit maakt workflows met Temporal makkelijk schaalbaar.

 

Enough talk, show us some code!

Hieronder een kort voorbeeld van een workflow. Ik maak zelf gebruik van de Java-client.

@WorkflowInterface
public interface GreetingWorkflow {
    @WorkflowMethod
    void greet();

    @SignalMethod
    void changeName(String name);

    @SignalMethod
    void terminate();

    @QueryMethod
    String getCurrentName();
}

 
public class GreetingWorkflowImpl implements GreetingWorkflow {

    private final GreetingActivities greetingActivities = Workflow.newActivityStub(
            GreetingActivities.class,
            ActivityOptions.newBuilder()
                    .setStartToCloseTimeout(Duration.ofMinutes(10L))
                    .validateAndBuildWithDefaults()
    );

    private String name = "Stranger";
    private boolean active = true;

    @Override
    public void greet() {
        while (active) {
            String oldName = name;
            greetingActivities.sayHi(name);
            Workflow.await(() -> !Objects.equals(oldName, name) || !active);
        }
    }

    @Override
    public void changeName(String name) {
        this.name = name;
    }

    @Override
    public void terminate() {
        this.active = false;
    }

    @Override
    public String getCurrentName() {
        return this.name;
    }
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Een workflow maken: hoe werkt dat?

Een workflow is altijd een interface, waarvan je vervolgens een implementatie maakt. Een implementatie kan uit meerdere interfaces bestaan. Wat belangrijk is, is dat de workflow één method bevat met een @WorkflowMethod annotatie. De code binnen deze workflow is de beschrijving van jouw proces, van begin tot eind. Als de method in zijn geheel is uitgevoerd, is de workflow afgerond. In het voorbeeld hierboven wordt een naam begroet en wordt elke keer dat de naam wordt gewijzigd een nieuwe begroeting gestuurd. De flow eindigt als de ‘active’ vlag op false wordt gezet.

Een tweede soort method die een workflow kan bevatten is de @SignalMethod. Deze methods kunnen worden gebruikt om de workflow een externe trigger te geven. Denk bijvoorbeeld aan een trigger vanuit een bericht op een queue of een verzoek van de gebruiker. In het voorbeeld hierboven zijn twee @SignalMethods gedefinieerd: één om de naam die begroet moet worden te wijzigen en één om de ‘active’ vlag op false te zetten (wat de workflow zal beëindigen).

Als laatste zijn er methods met een @QueryMethod annotatie. Deze kunnen worden gebruikt om data uit de workflow op te halen. Dit zal meestal een onderdeel van de state van de workflow zijn. In het voorbeeld hierboven wordt deze method gebruikt om de huidige naam uit de workflow state op te halen.

Hoe maak ik Activities?

De bijbehorende Activities zien er in de code als volgt uit:

@ActivityInterface
public interface GreetingActivities {
    void sayHi(String name);
}
 
public class GreetingActivitiesImpl implements GreetingActivities {

    private final PrintStream printStream;

    public GreetingActivitiesImpl(PrintStream printStream) {
        this.printStream = printStream;
    }

    @Override
    public void sayHi(String name) {
        printStream.println("Hi " + name);
    }
}

 

Ook hier maak je een interface met implementatie. Met de annotatie @ActivityInterface geef je aan dat deze interface een of meerdere Activities bevat. Binnen de workflow gebruik je de Activity door een ActivityStub aan te maken:

 

private final GreetingActivities greetingActivities = Workflow.newActivityStub(
            GreetingActivities.class,
            ActivityOptions.newBuilder()
                    .setStartToCloseTimeout(Duration.ofMinutes(10L))
                    .validateAndBuildWithDefaults()
    );
 

 

Deze kun je vervolgens als volgt binnen je workflow gebruiken:

greetingActivities.sayHi(“Jan”);

 

Een Temporal workflow starten

Om je workflows te kunnen starten heb je een WorkflowClient nodig:

 

var workflowServiceStubOptions = WorkflowServiceStubsOptions.newBuilder()
    .setTarget("localhost:7233")
    .setRpcTimeout(Duration.of(60, ChronoUnit.SECONDS))
    .setQueryRpcTimeout(Duration.of(10, ChronoUnit.SECONDS))
    .validateAndBuildWithDefaults();

var workflowServiceStubs = WorkflowServiceStubs.newInstance(workflowServiceStubsOptions);

var workflowClientOptions = WorkflowClientOptions.newBuilder()
                .setNamespace("TestNameSpace")
                .validateAndBuildWithDefaults();

var workflowClient = WorkflowClient.newInstance(workflowServiceStubs, workflowClientOptions);

 

 

Om je implementaties te laten uitvoeren, heb je workers nodig. Bij deze workers worden de implementaties van de betreffende workflows en activities vastgelegd:

 

var workerFactoryOptions = WorkerFactoryOptions.newBuilder().build();
var workerFactory = WorkerFactory.newInstance(workflowClient, workerFactoryOptions);
var worker = workerFactory.newWorker("TaskList", workerOptions);
 
worker.registerWorkflowImplementationTypes(new Class[] { GreetingWorkflowImpl.class });
worker.registerActivitiesImplementations(new Object[] { new GreetingActivitiesImpl(System.out) });

 

Start vervolgens je workerFactory om de workers te laten beginnen met pollen: workerFactory.start();

Een workflow kun je vervolgens starten door een WorkflowStub te creëren en de WorkflowClient.start() method aan te roepen.

 

var workflowOptions = WorkflowOptions.newBuilder()
    .setWorkflowExecutionTimeout(Duration.of(60, ChronoUnit.SECONDS))
    .setTaskQueue("TaskList")
     .build();

GreetingWorkflow workflow = workflowClient.newWorkflowStub(GreetingWorkflow.class, workflowOptions);
WorkflowClient.start(workflow::greet);

 
worker.registerWorkflowImplementationTypes(new Class[] { GreetingWorkflowImpl.class });
worker.registerActivitiesImplementations(new Object[] { new GreetingActivitiesImpl(System.out) });

 

Waarom gebruik je stubs?

De oplettende lezer heeft zich vast afgevraagd waarom er voor het gebruiken van workflows en activities stubs worden gebruikt. Ik zal hier nu niet te diep op ingaan. Kort gezegd: dit heeft te maken met de manier waarop Temporal werkt. Bij het starten van je applicatie registreer je de implementaties van je Workflow en Activity interfaces bij de workers. Zo weten de workers, als ze een taak oppakken, welke code gebruikt moet worden. Op het moment dat je een stub aanmaakt via de WorkflowClient, wordt er een proxy gebruikt, die de Temporal service aanroept. Deze service verdeelt de aangeroepen taken onder de pollende workers, waar de aangeroepen functie uiteindelijk wordt uitgevoerd. Bij een multi-instance deployment kan het dus zijn dat de logica door een andere instance wordt opgepakt.

 

Meer weten?

In deze blog heb ik geprobeerd een tipje van de sluier op te lichten van het Temporal framework. Meer meer informatie kun je kijken op https://temporal.io of https://github.com/temporalio/temporal. Ook kun je eens kijken naar dit voorbeeld op mijn GitHub, waarin ik Temporal in een Spring Boot-applicatie heb geïntegreerd: https://github.com/frtelg/temporal-spring-boot. Hier is ook het voorbeeld uit deze blog in opgenomen.