Once upon a time...
A customer had issues with Magento 2 webshop...
...quirks of CatalogInventory implementation
SELECT ... FOR UPDATE on every sale
Price & Stock re-indexation on product changing stock status
Magento 2.2 has only one qty field - that is the single truth for the whole process
Magento MSI has multiple fields, but has a race condition that allows to bypass qty check
MSI clears all history of mutations
public function execute(): void
{
$connection = $this->resource->getConnection();
$reservationTable = $this->resource->getTableName('inventory_reservation');
$select = $connection->select()
->from(
$reservationTable,
['GROUP_CONCAT(' . ReservationInterface::RESERVATION_ID . ')']
)
->group([ReservationInterface::STOCK_ID, ReservationInterface::SKU])
->having('SUM(' . ReservationInterface::QUANTITY . ') = 0');
$connection->query('SET group_concat_max_len = ' . $this->groupConcatMaxLen);
$groupedReservationIds = implode(',', $connection->fetchCol($select));
$condition = [
ReservationInterface::RESERVATION_ID . ' IN (?)'
=> explode(',', $groupedReservationIds)
];
$connection->delete($reservationTable, $condition);
}
So I had 3 different options to fix customer's issue
Completely disable stock processing
Ping ERP on every stock calculation
Build an async PHP application cluster with EventSourcing
Epoch
+------------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+------------------+------+-----+---------+----------------+
| epoch_id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| started_at | datetime | NO | MUL | NULL | |
| ended_at | datetime | YES | | NULL | |
+------------+------------------+------+-----+---------+----------------+
Epoch Events
+--------------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------------+------------------+------+-----+---------+----------------+
| event_id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| occured_at | datetime | NO | MUL | NULL | |
| event_type | varchar(255) | NO | | NULL | |
| aggregate_id | varchar(255) | NO | MUL | NULL | |
| event_data | text | NO | | NULL | |
+--------------+------------------+------+-----+---------+----------------+
Aggregate
+------------------+---------------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+------------------+---------------------+------+-----+---------+-------+
| aggregate_id | varchar(255) | NO | PRI | NULL | |
| last_event_id | int(10) unsigned | NO | | NULL | |
| last_event_epoch | int(10) unsigned | NO | MUL | NULL | |
| qty_reserved | int(11) | NO | | 0 | |
| qty_on_hand | int(11) | NO | | 0 | |
| is_in_stock | tinyint(3) unsigned | YES | | 0 | |
+------------------+---------------------+------+-----+---------+-------+
Choosing Responsible Worker
ReactPHP HTTP Server
https://github.com/reactphp/http
ReactPHP MySQL connector
@IvanChepurnyi
ivan@ecomdev.org