def test_concurrent_updates_to_version_are_not_allowed(postgres_session_factory): sku,batch = random_sku(),random_batchref() session = postgres_session_factory() insert_batch(session,batch,sku,100,None,product_version=1) session.commit() order1,order2 = random_orderid(1),random_orderid(2) exceptions = [] try_to_allocate_order1 = lambda :try_to_allocate(order1,sku,exceptions) try_to_allocate_order2 = lambda :try_to_allocate(order2,sku,exceptions) thread1 = threading.Thread(target=try_to_allocate_order1) thread2 = threading.Thread(target=try_to_allocate_order2) thread1.start() thread2.start() thread1.join() thread2.join() [[version]] = session.execute( 'select version_number from products where sku=:sku', dict(sku=sku) ) assert version == 2 [exception] = exceptions assert 'could not serialize access due to concurrent update' in str(exception) orders = list(session.execute( ' select order_id from allocations ' ' join batches on allocations.batch_id = batches.id' ' join order_lines on allocations.orderline_id=order_lines.id' ' where order_lines.sku=:sku', dict(sku=sku) )) assert len(orders) == 1 with unit_of_work.SqlAlchemyUnitOfWork() as uow: uow.session.execute('select 1')
def bootstrap( start_orm: bool = True, uow: unit_of_work.AbstractUnitOfWork = unit_of_work.SqlAlchemyUnitOfWork(), send_mail: Callable = email.send_mail, publish: Callable = event_publisher.publish, ) -> messagebus.MessageBus: if start_orm: orm.start_mappers() dependencies = {"uow": uow, "send_mail": send_mail, "publish": publish} injected_event_handlers = { event_type: [ inject_dependencies(event_handler, dependencies) for event_handler in event_handlers ] for event_type, event_handlers in handlers.EVENT_HANDLERS.items() } injected_commans_handlers = { command_type: inject_dependencies(command_handler, dependencies) for command_type, command_handler in handlers.COMMAND_HANDLERS.items() } return messagebus.MessageBus( uow=uow, event_handlers=injected_event_handlers, command_handlers=injected_commans_handlers, )
def test_rolls_back_on_error(sqlite_session_factory): class MyException(Exception): pass uow = unit_of_work.SqlAlchemyUnitOfWork(sqlite_session_factory) with pytest.raises(MyException): with uow:
def test_concurrent_updates_to_version_are_not_allowed( postgres_session_factory): sku, batch = random_sku(), random_batchref() session = postgres_session_factory() insert_batch(session, batch, sku, 100, eta=None, product_version=1) session.commit() order1, order2 = random_orderid(), random_orderid() exceptions = [] # type: List[Exception] try_to_allocate_order1 = lambda: try_to_allocate(order1, sku, exceptions) try_to_allocate_order2 = lambda: try_to_allocate(order2, sku, exceptions) thread1 = threading.Thread(target=try_to_allocate_order1) thread2 = threading.Thread(target=try_to_allocate_order2) thread1.start() thread2.start() thread1.join() thread2.join() [[version]] = session.execute( 'SELECT version_number FROM "products" WHERE sku=:sku', dict(sku=sku)) assert version == 2 [exception] = exceptions assert 'could not serialize access due to concurrent update' in str( exception) orders = list( session.execute( 'SELECT orderid FROM allocations' ' JOIN batches ON allocations.batch_id = batches.id' ' JOIN order_lines ON allocations.orderline_id = order_lines.id' ' WHERE order_lines.sku=:sku', dict(sku=sku))) assert len(orders) == 1 with unit_of_work.SqlAlchemyUnitOfWork() as uow: uow.session.execute('SELECT 1')
def test_concurrent_updates_to_version_are_not_allowed(mysql_session_factory): sku, batch = random_sku(), random_batchref() session = mysql_session_factory() insert_batch(session, batch, sku, 100, eta=None, product_version=1) session.commit() order1, order2 = random_orderid(1), random_orderid(2) exceptions = [] # type: List[Exception] try_to_allocate_order1 = lambda: try_to_allocate(order1, sku, exceptions) try_to_allocate_order2 = lambda: try_to_allocate(order2, sku, exceptions) thread1 = threading.Thread(target=try_to_allocate_order1) thread2 = threading.Thread(target=try_to_allocate_order2) thread1.start() thread2.start() thread1.join() thread2.join() [[version] ] = session.execute("SELECT version_number FROM products WHERE sku=:sku", dict(sku=sku)) assert version == 2 orders = session.execute( "SELECT orderid FROM allocations" " JOIN batches ON allocations.batch_id = batches.id" " JOIN order_lines ON allocations.orderline_id = order_lines.id" " WHERE order_lines.sku=:sku", dict(sku=sku)) [exception] = exceptions assert "Deadlock found when trying to get lock" in str(exception) assert orders.rowcount == 1 with unit_of_work.SqlAlchemyUnitOfWork() as uow: uow.session.execute("SELECT 1")
def bootstrap( start_orm: bool = True, uow: unit_of_work.AbstractUnitOfWork = unit_of_work.SqlAlchemyUnitOfWork(), notifications: AbstractNotifications = None, publish: Callable = redis_eventpublisher.publish, ) -> messagebus.MessageBus: if notifications is None: notifications = EmailNotifications() if start_orm: orm.start_mappers() dependencies = { 'uow': uow, 'notifications': notifications, 'publish': publish } injected_event_handlers = { event_type: [ inject_dependencies(handler, dependencies) for handler in event_handlers ] for event_type, event_handlers in handlers.EVENT_HANDLERS.items() } injected_command_handlers = { command_type: inject_dependencies(handler, dependencies) for command_type, handler in handlers.COMMAND_HANDLERS.items() } return messagebus.MessageBus( uow=uow, event_handlers=injected_event_handlers, command_handlers=injected_command_handlers, )
def sqlite_bus(sqlite_session_factory): bus = bootstrap.bootstrap( start_orm=True, uow=unit_of_work.SqlAlchemyUnitOfWork(sqlite_session_factory), notifications=mock.Mock(), publish=lambda *args: None) yield bus clear_mappers()
def messagebus(session_factory): bus = bootstrap.bootstrap( start_orm=False, uow=unit_of_work.SqlAlchemyUnitOfWork(session_factory), send_mail=lambda *args, **kwargs: None, publish=lambda *args, **kwargs: None, ) return bus
def try_to_allocate(orderid, sku, exceptions, session_factory): line = model.OrderLine(orderid, sku, 10) try: with unit_of_work.SqlAlchemyUnitOfWork(session_factory) as uow: product = uow.products.get(sku=sku) product.allocate(line) time.sleep(0.2) uow.commit()
def test_rolls_back_uncommited_work_by_default(session_factory): uow = unit_of_work.SqlAlchemyUnitOfWork(session_factory) with uow: insert_batch(uow.session, "batch1", "MEDIUM-PLINTH", 100, None) new_session = session_factory() rows = list(new_session.execute("SELECT * FROM batches")) assert rows == []
def add_batch(): uow = unit_of_work.SqlAlchemyUnitOfWork() eta = request.json["eta"] if eta is not None: eta = datetime.fromisoformat(eta).date() services.add_batch(request.json["ref"], request.json["sku"], request.json["qty"], eta, uow) return "OK", 201
def bus(sqlite_session_factory): bus = bootstrap.bootstrap( start_orm=True, uow=unit_of_work.SqlAlchemyUnitOfWork(sqlite_session_factory), notifications=notifications.EmailNotifications(), publish=lambda *args: None, update_readmodel=lambda *args: None) yield bus clear_mappers()
def test_rollback_unicommitted_work_by_default(sqlite_session_factory): uow = unit_of_work.SqlAlchemyUnitOfWork(sqlite_session_factory) with uow: insert_batch(uow.session, 'b1', '椅子', 100, None) new_session = sqlite_session_factory() rows = list(new_session.execute( 'select * from "batches"' )) assert rows == []
def test_rolls_back_uncommitted_work_by_default(sqlite_session_factory): uow = unit_of_work.SqlAlchemyUnitOfWork(sqlite_session_factory) with uow: insert_batch(uow.session, 'batch1', 'MEDIUM-PLINTH', 100, None) new_session = sqlite_session_factory() rows = list(new_session.execute('SELECT * FROM `batches`')) assert rows == []
def test_deallocation(sqlite_session_factory): uow = unit_of_work.SqlAlchemyUnitOfWork(sqlite_session_factory) messagebus.handle(commands.CreateBatch("b1", "sku1", 50, None), uow) messagebus.handle(commands.CreateBatch("b2", "sku1", 50, today), uow) messagebus.handle(commands.Allocate("o1", "sku1", 40), uow) messagebus.handle(commands.ChangeBatchQuantity("b1", 10), uow) assert views.allocations("o1", uow) == [ {"sku": "sku1", "batchref": "b2"}, ]
def allocation_endpoint(): uow = unit_of_work.SqlAlchemyUnitOfWork() try: batchref = services.allocate(request.json["orderid"], request.json["sku"], request.json["qty"], uow) except (model.OutOfStock, services.InvalidSku) as e: return jsonify({"message": str(e)}), 400 return jsonify({"batchref": batchref}), 201
def try_to_allocate(orderid, sku, exceptions): line = model.OrderLine(orderid, sku, 10) try: with unit_of_work.SqlAlchemyUnitOfWork() as uow: product = uow.products.get(sku=sku) product.allocate(line) time.sleep(0.2) uow.commit() except Exception as e: print(traceback.format_exc()) exceptions.append(e)
def test_rolls_back_on_error(session_factory): class MyException(Exception): pass uow = unit_of_work.SqlAlchemyUnitOfWork(session_factory) with pytest.raises(MyException): with uow: insert_batch(uow.session, "batch1", "LARGE-FORK", 100, None) raise MyException() new_session = session_factory() rows = list(new_session.execute("SELECT * FROM batches")) assert rows == []
def allocate_endpoint(): try: batchref = services.allocate( request.json['orderid'], request.json['sku'], request.json['qty'], unit_of_work.SqlAlchemyUnitOfWork(), ) except (model.OutOfStock, services.InvalidSku) as e: return jsonify({'message': str(e)}), 400 return jsonify({'batchref': batchref}), 201
def bootstrap( start_orm: bool = True, uow: unit_of_work.AbstractUnitOfWork = unit_of_work.SqlAlchemyUnitOfWork(), notifications: AbstractNotifications = None, publish: Callable = redis_eventpublisher.publish, ) -> messagebus.MessageBus: if notifications is None: notifications = EmailNotifications() if start_orm: orm.start_mappers()
def add_batch(): eta = request.json['eta'] if eta is not None: eta = datetime.fromisoformat(eta).date() services.add_batch( request.json['ref'], request.json['sku'], request.json['qty'], eta, unit_of_work.SqlAlchemyUnitOfWork(), ) return 'OK', 201
def allocate_endpoint(): try: cmd = commands.Allocate( request.json["orderid"], request.json["sku"], request.json["qty"], ) uow = unit_of_work.SqlAlchemyUnitOfWork() messagebus.handle(cmd, uow) except InvalidSku as e: return jsonify({"message": str(e)}), 400 return "OK", 202
def add_batch(): eta = request.json["eta"] if eta is not None: eta = datetime.fromisoformat(eta).date() cmd = commands.CreateBatch( request.json["ref"], request.json["sku"], request.json["qty"], eta, ) uow = unit_of_work.SqlAlchemyUnitOfWork() messagebus.handle(cmd, uow) return "OK", 201
def test_deallocation(sqlite_session_factory): uow = unit_of_work.SqlAlchemyUnitOfWork(sqlite_session_factory) messagebus.handle(commands.CreateBatch('b1', 'sku1', 50, None), uow) messagebus.handle(commands.CreateBatch('b2', 'sku1', 50, today), uow) messagebus.handle(commands.Allocate('o1', 'sku1', 40), uow) messagebus.handle(commands.ChangeBatchQuantity('b1', 10), uow) assert views.allocations('o1', uow) == [ { 'sku': 'sku1', 'batchref': 'b2' }, ]
def add_batch(): eta = request.json['eta'] if eta is not None: eta = datetime.fromisoformat(eta).date() cmd = commands.CreateBatch( request.json['ref'], request.json['sku'], request.json['qty'], eta, ) uow = unit_of_work.SqlAlchemyUnitOfWork() messagebus.handle(cmd, uow) return 'OK', 201
def test_uow_can_retreive_a_batch_and_allocate_to_it(session_factory): session = session_factory() insert_batch(session, "batch1", "HIPSTER-WORKBENCH", 100, None) session.commit() uow = unit_of_work.SqlAlchemyUnitOfWork(session_factory) with uow: batch = uow.batches.get(reference="batch1") line = model.OrderLine("o1", "HIPSTER-WORKBENCH", 10) batch.allocate(line) uow.commit() batchref = get_allocated_batch_ref(session, "o1", "HIPSTER-WORKBENCH") assert batchref == "batch1"
def test_uow_can_retrieve_a_batch_and_allocate_to_it(session_factory): session = session_factory() insert_batch(session, 'batch1', 'HIPSTER-WORKBENCH', 100, None) session.commit() uow = unit_of_work.SqlAlchemyUnitOfWork(session_factory) with uow: product = uow.products.get(sku='HIPSTER-WORKBENCH') line = model.OrderLine('o1', 'HIPSTER-WORKBENCH', 10) product.allocate(line) uow.commit() batchref = get_allocated_batch_ref(session, 'o1', 'HIPSTER-WORKBENCH') assert batchref == 'batch1'
def allocate_endpoint(): try: cmd = commands.Allocate( request.json['orderid'], request.json['sku'], request.json['qty'], ) uow = unit_of_work.SqlAlchemyUnitOfWork() results = messagebus.handle(cmd, uow) batchref = results.pop(0) except InvalidSku as e: return jsonify({'message': str(e)}), 400 return jsonify({'batchref': batchref}), 201
def test_uow_can_retrieve_a_batch_and_allocate_to_it(sqlite_session_factory): session = sqlite_session_factory() insert_batch(session, "batch01", "HIPSTER-WORKBENCH", 100, None) session.commit() uow = unit_of_work.SqlAlchemyUnitOfWork(sqlite_session_factory) with uow: product = uow.products.get(sku="HIPSTER-WORKBENCH") line = model.OrderLine("o1", "HIPSTER-WORKBENCH", 10) product.allocate(line) time.sleep(0.2) uow.commit() batchref = get_allocated_batch_ref(session, "o1", "HIPSTER-WORKBENCH") assert batchref == "batch01"
def test_allocations_view(sqlite_session_factory): uow = unit_of_work.SqlAlchemyUnitOfWork(sqlite_session_factory) messagebus.handle(commands.CreateBatch("sku1batch", "sku1", 50, None), uow) messagebus.handle(commands.CreateBatch("sku2batch", "sku2", 50, today), uow) messagebus.handle(commands.Allocate("order1", "sku1", 20), uow) messagebus.handle(commands.Allocate("order1", "sku2", 20), uow) # add a spurious batch and order to make sure we're getting the right ones messagebus.handle(commands.CreateBatch("sku1batch-later", "sku1", 50, today), uow) messagebus.handle(commands.Allocate("otherorder", "sku1", 30), uow) messagebus.handle(commands.Allocate("otherorder", "sku2", 10), uow) assert views.allocations("order1", uow) == [ {"sku": "sku1", "batchref": "sku1batch"}, {"sku": "sku2", "batchref": "sku2batch"}, ]