Skip to content

Reservations

Igor Miniailo edited this page Aug 17, 2017 · 70 revisions

Table of Contents

Reservation Mechanism and its APIs

Reservation - the entity responsible for reservations, created to keep inventory amount (product quantity) up-to-date. It is created to have a state between order creation and inventory deduction (deduction of specific SourceItems). At the following status, nothing is deducted from SourceItem records. Introducing Reservation entity we could be sure that we would not sell more products than we have in our inventory even if latency between order placement and order processing (deduction of specific SourceItems) is high. Reservations are append only operations and help us to prevent blocking operations and race conditions at the time of checkout.

Description of Order Placement operation Step by Step

Initial conditions.

There are 3 physical sources: Source A, Source B, Source C and There is Product with SKU-1 which is stored on each of these sources in next quantity:

  • SourceItem A — 20
  • SourceItem B — 25
  • SourceItem C — 10

There is the only sales channel (Default Website). For this sales channel, we create a virtual aggregated stock - Stock A and assign all existing physical sources to it. Thus, StockItem A for SKU-1 has the Quantity 20+25+10 = 55

When a customer comes to the website, the system detects Stock which should be used (in our case Website -> Stock A), in the scope of this Stock we choose StockItems for product quantity retrieval. In our case for SKU-1

Order placement

Let's assume customer comes to Default Website and wants to buy product SKU-1 in the amount of 30 items.

Magento needs to decide whether we can sell (do we have enough products to sell in our inventory), Quantity of SKU-1 on StockItem A is 55, plus an aggregated quantity of all the reservations for product SKU-1 on Stock A. In our case there are no reservations created, so the number is 0, 55 - 0 > 30, so we can proceed to checkout and place order.

At the time of order placement, the system is agnostic to the fact from which physical sources the qty of SKU-1 would be deducted afterward, that's why we don't use SourceItem interfaces during this process (order placement). Also, we can't deduct Qty of StockItem A, because it's read-only interface and represents index value. Thus, we create a Reservation for SKU-1 on Stock A in the amount of (-30) items. Reservation creation is append-only operation, so there are no checks and blocking operations (locks) needed.

The currect state of the system after first order has been placed (but not yet processed):

Amount of SKU-1 on physical sources:

  • SourceItem A — 20
  • SourceItem B — 25
  • SourceItem C — 10

The quantity of SKU-1 on StockItem A — 55 (has not changed) Reservation for SKU-1 on Stock A created in the amount of (-30) items.

While we didn't process first order yet, because of high latency, another customer comes to the website and wants to order SKU-1 in amount of 10 items.

Magento starts to follow the steps mentioned above. Magento needs to decide whether we can sell (do we have enough products to sell in our inventory), Quantity of SKU-1 on StockItem A is 55, plus an aggregated quantity of all the reservations for product SKU-1 on Stock A 55 + (-30) = 25 > 10 Thus, we make a decision that we can proceed to checkout.

Binding Reservation to Order. And Order Cancellation

We no need to bind a Reservation to Order placed (by order id, or order items), because that could introduce additional complexity and locks. Moreover, we will create Reservation when RMA (Returns) would be created. Thus, Order is not the only business logic which emits the reservations. We see Reservation as append-only operation. Like a log of events (in Event Sourcing terms). Our stock calculation for product(SKU) is next: get StockItem Quantity (which represents aggregated amount among all the physical sources for the current Scope/SalesChannel) for particular SKU plus all created reservations for this SKU made in the same Scope/SalesChannel. So, let’s imagine that Customer A bought 30 items of some product - we create a reservation for this sale.

ReservationID - 1 , StockId - 1, SKU - SKU-1, Qty - (-30)

if the order is canceled we could just create another Reservation

ReservationID - 2 , StockId - 1, SKU - SKU-1, Qty - (+30)

So, we don't remove or modify already created reservations, just append another one, which makes quantity correction.

So, the second reservation would neglect the 1st one Like (-30) + 30 = 0

Because from the calculation point of view it would be easier to have both negative (<0) and positive (>0) Qty values in Reservations. Like when we placed an order -> we created Reservation with Qty -30, when we processed the Order and deducted SourceItems -> we created a reservation with Qty +30 That will provide an efficient way of how can we get Sum of Grouped Reservations. For example, executing this query:

select 
   SUM(r.qty) as total_reservation_qty
from 
   Reservations as r
where 
  stockId = {%id%} and sku  = {%sku%}

pretty simple query, and we've given total reservation qty.

if we would have always Qty > 0, we need more sophisticated logic i.e.

  • process aggregation query for Open Reservation
  • process aggregation query for Closed Reservation
  • subtract 1. from 2.

Reservation API

Data Interface

<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Magento\InventoryApi\Api\Data;

use Magento\Framework\Api\ExtensibleDataInterface;
use Magento\InventoryApi\Api\Data\ReservationExtensionInterface;

/**
 * The entity responsible for reservations, created to keep inventory amount (product quantity) up-to-date.
 * It is created to have a state between order creation and inventory deduction (deduction of specific SourceItems)
 *
 * @api
 */
interface ReservationInterface
{
    /**
     * Constants for keys of data array. Identical to the name of the getter in snake case
     */
    const RESERVATION_ID = 'reservation_id';
    const STOCK_ID = 'stock_id';
    const SKU = 'sku';
    const QUANTITY = 'quantity';
    const METADATA = 'metadata';

    /**
     * Get Reservation id
     *
     * @return int|null
     */
    public function getReservationId();

    /**
     * Get stock id
     *
     * @param int $stockId
     * @return void
     */
    public function getStockId($stockId);

    /**
     * Get Product SKU
     *
     * @return string
     */
    public function getSku();

    /**
     * Get Product Qty
     *
     * @return float
     */
    public function getQuantity();

    /**
     * Get Reservation Metadata
     *
     * @return string
     */
    public function getMetadata();
}

We no need to expose Reservation API for WebAPI (REST and SOAP), because we can consider Reservations as SPI, which created as a side-effect of some particular business operation (like order placement, or return). Currently, in Magento 2 WebAPI imposes some restrictions for entity interfaces (existence getter and setter methods). Thus, if we would not expose Reservation entity for WebAPI (REST, SOAP) -> we could use any interface we want (don't have mandatory setter methods). And because we agreed that Reservations are append-only immutable entities we could eliminate all the setter methods. So, we will end-up with ReservationInterface consisting of just getter methods. And we need to introduce ReservationBuilderInterface which will allow the possibility to set data into the reservation when we need to create one. after that, we could build Reservation entity.

$reservationBuilder->setStockId(1);
$reservationBuilder->setSku('sku');
$reservationBuilder->setQty(10);
$newReservation = $reservationBuilder->build();
//now we could save Reservation entity 
$reservationAppend->execute([$newReservation]);

Doing so, we could ensure immutability on the level of Reservation interface.

Reservation Services

Append Reservation Service - used at a time when Order placed or canceled. At this time, we create a bunch of Reservations, each one responsible for particular SKU And add these reservations for processing. This service must ensure that client doesn't use ReservationAppend service to update already created reservations. Because Reservations are append-only entities. For example, if we will use Database generated IDs, we could check the ReservationId which is passed in the scope of ReservationInterface is nullified. Or if UUIDs are used, we could check that there are no reservations with the same UUID placed already.

/**
 * Command which appends reservations when order placed or canceled
 *
 * @api
 */
interface ReservationAppend
{
    /**
     * Append reservations when Order Placed (or Cancelled)
     *
     * @param Reservation[] $reservations
     * @return void
     * @throws \Magento\Framework\Exception\InputException
     * @throws \Magento\Framework\Exception\CouldNotSaveException
     */
    public function execute(array $reservations);
}

To get Product Quantity available to be sold for specified Stock we need to introduce another service which will use StockItem Qty and deduct all existing reservations for this SKU in provided scope (StockId).

/**
 * Command which returns Quantity of products available to be sold by Product SKU and Stock Id
 *
 * @api
 */
interface GetProductQuantityInStock
{
    /**
     * Get Product Quantity for given SKU in a given Stock
     *
     * @param string $sku
     * @param int $stockId
     * @return float
     */
    public function execute($sku, $stockId);
}

Removal of processed Reservations

At the time of StockItem re-indexation it's too late to clear Reservations. Because StockItem index accepts SourceItem records which were updated as the result of Source Selection Algorithm. And it's impossible to detect in which scope (sales channel/stock id) the order has been placed. Thus, it's impossible to decide which Reservations should be removed, because Reservation created in the scope of stock_id (which is absent at this moment, Source could be assigned to More than 1 Stock).

That's why we need to create NEW reservation at the Order processing time when we processed the Order and got a result from Source Selection Algorithm (Sources we will use to fulfill the order placed, and the precise quantity of SKUs we need to deduct from the SourceItems).

As reservations are append-only operations it's proposed not to modify the status of created Reservation, but add another reservation which neglects already existing Reservation (like in the example above -30 +30 = 0). From the inventory point of view we don't bind Reservation to Order or other business operation, that's why we don't introduce Reservation Statuses (and apply State Machine design pattern for changing the reservation from one state to another one). All we need to do is to create another reservation. That's all.

Order Placed for SKU-1 in Qty = 30 => Created Reservation for SKU-1 with Qty = (-30)
Canceled above order =>  Created Reservation for SKU-1 with Qty = (+30)
or
Completed above order => Created Reservation for SKU-1 with Qty = (+30)

Idea is to clear reservation table (if needed) to prevent overloading, finding Complete pairs of reservations. When we have a pair of reservations, the sum of which is equal to O, like -30 and +30. These two reservations don't affect the final quantity, thus could be deleted.

Launching a script periodically we could find such pairs and remove them from the table not affecting the calculation.

select 
   reservation_id, qty
from 
   Reservations as r
where
   stockId = {%id%} and sku  = {%sku%}

After executing this query we will get a list of all created Reservations for the product in a given scope. Looping through these reservations we could find pairs which in sum gives 0 and remove them.

It doesn't matter how fast is above processing (how much time takes to proceed one) because it's launched for service purposes only to remove unneeded reservations.

MSI Documentation:

  1. Technical Vision. Catalog Inventory
  2. Installation Guide
  3. List of Inventory APIs and their legacy analogs
  4. MSI Roadmap
  5. Known Issues in Order Lifecycle
  6. MSI User Guide
  7. DevDocs Documentation
  8. User Stories
  9. User Scenarios:
  10. Technical Designs:
  11. Admin UI
  12. MFTF Extension Tests
  13. Weekly MSI Demos
  14. Tutorials
Clone this wiki locally