reservation.request

Model.Wms.Reservation.Request

class anyblok_wms_base.reservation.request.Request[source]

Fields and their semantics

id = <anyblok.column.Integer object>

Primary key.

In this model, the ordering of id ordering is actually important (whereas on many others, it’s a matter of habit to have a serial id): the smaller it is, the older the Request.

Requests have to be reserved in order.

Note that serial columns in PostgreSQL don’t induce conflicts, as the sequence is evaluated out of transaction.

purpose = <anyblok_postgres.column.Jsonb object>

Flexible field to describe what the reservations will be for.

This is typically used by a planner, to produce an appropriate chain of Operations to fulfill that purpose.

Example: in a simple sales system, we would record a sale order reference here, and the planner would then take the related PhysObj and issue (planned) Moves and Departures for their ‘present’ or ‘future’ Avatars.

reserved = <anyblok.column.Boolean object>

Indicates that all reservations are taken.

TODO: find a way to represent if the Request is partially done ? Some use-cases would require planning partial deliveries and the like in that case.

planned = <anyblok.column.Boolean object>

Indicates that the planner has finished with that Request.

It’s better than deleting, because it allows to cancel all Operations, set this back to True, and plan again.

Methods

classmethod claim_reservations(query=None, **filter_by)[source]

Context manager to claim ownership over this Request’s reservations.

This is meant for planners and works on fully reserved Requests. Example:

Request = registry.Wms.Reservation.Request
with Request.claim_reservations() as req_id:
    request = Request.query().get(req_id)
    (...) read request.purpose, plan Operations (...)

By calling this, the current transaction becomes responsible for all Request’s reservations, meaning that it has the liberty to issue any Operation affecting its PhysObj or their Avatars.

Returns:

id of claimed Request

Parameters:
  • filter_by (dict) – direct filtering criteria to add to the query, e.g, a planner looking for planning to be done would pass planned=False.
  • query – if specified, is used to form the final SQL query, instead of creating a new one. The passed query must have the present model class in its FROM clause and return only the id column of the present model. The criteria of filter_by are still applied if also provided.

This is safe with respect to concurrency: no other transaction can claim the same Request (guaranteed by a PostgreSQL lock).

The session will forget about this Request as soon as one exits the with statement, and the underlying PG lock is released at the end of the transaction.

TODO for now it’s a context manager. I’d found it more elegant to tie it to the transaction, to get automatic release, without a with syntax, but that requires more digging into SQLAlchemy and Anyblok internals.

TODO I think FOR UPDATE actually creates a new internal PG row (table bloat). Shall we switch to advisory locks (see PG doc) with an harcoded mapping to an integer ? If that’s true, then performance-wise it’s equivalent for us to set the txn id in some service column (but that would require inconditional cleanup, a complication)

classmethod reserve_all(batch_size=10, nb_attempts=5, retry_delay=1, query_filter=None)[source]

Try and perform all reservations for pending Requests.

This walks all pending (reserved equal to False) Requests that haven’t been reserved from the oldest and locks them by batches of batch_size.

Reservation is attempted for each request, in order, meaning that each request will grab as much PhysObj as it can before the next one gets processed.

Parameters:
  • batch_size (int) – number of pending Requests to grab at each iteration
  • nb_attempts – number of attempts (in the face of conflicts) for each batch
  • retry_delay – time to wait before retrying to grab a batch (hoping other transactions holding locks would have released them)
  • query_filter – optional function to add filtering to the query used to grab the reservations. The caller can use this to implement controlled concurrency in the reservation process: several processes can focus on different Requests, as long as they don’t compete for PhysObj to reserve.

The transaction is committed for each batch, and that’s essential for proper operation under concurrency.

reserve()[source]

Try and perform reservation for all RequestItems.

Returns:True if all reservations are now taken
Return type:bool

Should not fail if reservations are already done.

Exceptions

ReservationsLocked = <class 'anyblok_wms_base.reservation.request.Request.ReservationsLocked'>[source]

Internal methods

is_txn_reservations_owner()[source]

Tell if transaction is the owner of this Request’s reservations.

Returns:True if the current transaction has claimed ownership, using the :meth:claim_reservations method.
classmethod lock_unreserved(batch_size, query_filter=None, offset=0)[source]

Take exclusivity over not yet reserved Requests

This is used in Reservers implementations.

Parameters:batch (int) – maximum of reservations to lock at once.

Since reservations have to be taken in order, this produces a hard error in case there’s a conflicting database lock, instead of skipping them like claim_reservations() does.

This conflicts in particular locks taken with :meth`claim_reservations`, but in principle, only reservers should take locks over reservation Requests that are not reserved yet, and these should not run in concurrency (or in a very controlled way, using query_filter).

Model.Wms.Reservation.RequestItem

class anyblok_wms_base.reservation.request.RequestItem[source]

Fields and their semantics

id = <anyblok.column.Integer object>

Primary key.

Note that serial columns in PostgreSQL don’t induce conflicts, as the sequence is evaluated out of transaction.

request = <anyblok.relationship.Many2One object>
goods_type = <anyblok.relationship.Many2One object>
quantity = <anyblok.column.Integer object>
properties = <anyblok_postgres.column.Jsonb object>

Methods

lookup(quantity)[source]

Try and find PhysObj matchin the specified conditions.

Returns:the matching PhysObj that were found and the quantity each accounts for. The PhysObj may not be of the requested type. What matters is how much of the requested quantity each one represents.
Return type:list(int, PhysObj <anyblok_wms_base/bloks/wms_core/goods.PhysObj>)

This method is where most business logic should lie.

This default implementation does only equal matching on PhysObj Type and each property, and therefore is not able to return other PhysObj Type accounting for more than one of the wished. Downstream libraries and applications are welcome to override it.

reserve()[source]

Perform the wished reservations.

Return bool:if the RequestItem is completely reserved. TODO: shall we store it directly in DB ?