Saga Orchestrator
Table Of Contents
Introduction
A saga is a data consistency maintaining mechanism used in distributed systems (such as microservices-based applications) each of which has own database, making it impossible to use ACID.
The saga pattern represents a local transactions sequence executing through asynchronous messaging communication.
It can be seen as an alternative to 2PC protocol saving important microservices architecture advantages such as the possibility of using an appropriate storage for concrete microservices nature, as well as asynchronous messaging for high availability and loose coupling. Also it increased flexibility and scalability due to avoidance of participants blocking.
There are two ways to coordinate the saga execution:
- choreography - services publish and subscribes to domain events;
- orchestration - execution is managed by special service that controls transactions sequence and said services that they must do. This framework implements that approach.
More details about sagas you can find on the Chris Richardson site or in his great book Microservices Patterns.
Requirements
- php: >= 7.1
- ext-json
- phpsagas/contracts
- psr/log: ^1.1
About package
This component is the heart of phpsagas framework and is responsible for central coordination of each saga participant local transactions execution. Implementation inspired by eventuate-tram-sagas framework. You can use the orchestrator by one of the following methods:
- as part of your project (just a vendor package) by including it in a service, which is a owner of distributed business-transaction;
- as a standalone project having own database.
This choice entirely dependent on your preferences. Each of the options carried advantages and drawbacks, for example usage of orchestrator as a separate service can provide possibility of another database usage, as well as deployment on high-performance hardware. However, there are some disadvantages such as an undesirable control logic centralization, as well as a single point of failure.
Usage of orchestrator as a project package is more simple and allows to reduce messaging (using current service transactions as local commands).
Installation
You can install the package using Composer:
composer require phpsagas/orchestrator
Getting started
Configuration
There are some interfaces you have to implement (or use existing implementations):
SagaFactoryInterface
- for sagas creation (test example)SagaInstanceRepositoryInterface
- sagas repository (doctrine implementation)MessagePayloadSerializerInterface
- serialize participant commands data messages (symfony/serializer implementation)SagaSerializerInterface
- serialize sagas data (symfony/serializer implementation)MessageProducerInterface
- messages sender (symfony/messenger implementation))MessageIdGeneratorInterface
- generate message ids (uuid implementation).
Next, it is necessary to configure base orchestrator services - Saga Creator
and SagaReplyHandler
. You can do it using your favourite service-container (symfony autowiring, pimple, PHP-DI, etc) or manually (see below).
After that, the orchestrator is ready to be used. Let's look how that works.
Saga creation
Saga must implement SagaInterface
by providing a type with definition consisting of steps that each distributed transaction participant has to perform. For example, let's consider Travel Tour Service. The tour buying may consist of next stages distributed by some services:
- hotel booking // Hotel Service
- tickets booking // Tickets Service
- visa obtaining // Visa Service
- tour buying // Tour Service with Orchestrator
So, BuyTourSaga
definition may seem as follows:
class BuyTourSaga
{
// ...
public function getSagaDefinition(): SagaDefinition
{
$steps = $this
->step()
->localCommand($buyTourCommand) // <-- compensatable transaction
->withCompensation($rejectTourCommand) // <-- compensating transaction
->step()
->remoteCommand($bookTicketsCommand)
->withCompensation($rejectTicketsBookingCommand)
->step()
->remoteCommand($bookHotelCommand)
->withCompensation($rejectHotelBookingCommand)
->step()
->remoteCommand($obtainVisaCommand) // <-- pivot transaction
->onReply($obtainVisaReplyHandler)
->step()
->remoteCommand($confirmHotelBookingCommand) // <-- retryable transaction
->step()
->remoteCommand($confirmTicketsBookingCommand)
->step()
->localCommand($confirmTourCommand);
;
return $steps->build();
}
public function onFinished(string $sagaId, SagaDataInterface $data): void
{
// notify request initiator about successful outcome
}
public function getSagaType(): string
{
return 'buy_tour_saga';
}
private function step(): StepBuilder
{
return new StepBuilder(new SagaDefinitionBuilder());
}
}
The state machine saga representation:
Be careful when creating steps sequence of the saga definition!
Let's say that the Visa Service is a third party project provided VisaObtain API only (with no possibility of cancellation). In that case all compensatable commands should be placed before the visa obtaining command being a pivot transaction defining the saga outcome (more details about transactions categories).
Also, the each saga lifecycle consists of creation, successful execution or failure that can include some logic implementation (e.g. using event dispatcher) as the request initiator notification upon the saga completion.
The saga can execute both local (same project with orchestrator) and remote commands. The main purpose of the command is to delegate business logic execution to application services and either update saga data (for local commands) or provide data for another microservice's logic execution (for remote commands).
A local command example:
class BuyTourCommand implements LocalCommandInterface
{
private $tourService;
// inject your project service
public function __construct(TourService $tourService)
{
$this->tourService = $tourService;
}
/**
* @param SagaDataInterface|BuyTourSagaData $sagaData
*/
public function execute(SagaDataInterface $sagaData): void
{
// buyTour logic incapsulated behind project service, not here
$tour = $this->tourService->buyTour(
$sagaData->getCountry(),
$sagaData->getCity(),
$sagaData->getDateFrom(),
$sagaData->getDateTill()
);
// set created tour id to saga data for next commands usage
$sagaData->setTourId($tour->getId());
}
public function getSagaDataType(): string
{
return BuyTourSagaData::class;
}
}
A remote command example:
class BookTicketsCommand implements RemoteCommandInterface
{
public function getCommandType(): string
{
return 'book_tickets_command';
}
public function getSagaDataClassName(): string
{
return BuyTourSagaData::class;
}
/**
* Returns data using by another application services.
*
* @param SagaDataInterface|BuyTourSagaData $sagaData
*
* @return CommandDataInterface
*/
public function getCommandData(SagaDataInterface $sagaData): CommandDataInterface
{
return new BookTicketsData(
$sagaData->getCountry(),
$sagaData->getCountry(),
$sagaData->getDateFrom(),
$sagaData->getDateTill()
);
}
Local commands results processing may be performed immediately after the current application service call (see above). In order to process remote commands execution results you have to use ReplyHandler:
class BookHotelReplyHandler implements ReplyHandlerInterface
{
/**
* @param ReplyMessage $message
* @param SagaDataInterface|BuyTourSagaData $sagaData
*/
public function handle(ReplyMessage $message, SagaDataInterface $sagaData): void
{
if ($message->isSuccess()) {
$payload = json_decode($message->getPayload(), true);
$sagaData->setHotelBookingId($payload['hotelBookingId']);
}
}
}
Internal
There are three main parts or the orchestrator:
BuildEngine
- responsible for saga definition and represents state on execution steps;InstantiationEngine
- provides methods for saga and saga instance creation;ExecutionEngine
- controls saga execution, manages saga state changes:SagaCreator
- starts saga execution;SagaReplyHandler
- performs remote commands handling (used by consumers);SagaActionsProcessor
- controls saga execution, updates and saves saga state.
Saga execution sequence: More saga usage details available in package tests.
License
Saga orchestrator is released under the MIT license.