|
16 | 16 |
|
17 | 17 | import doctest |
18 | 18 | import time |
| 19 | +import warnings |
| 20 | +import gc |
19 | 21 |
|
20 | 22 | from persistent import Persistent |
21 | 23 | from persistent.mapping import PersistentMapping |
22 | 24 | from ZODB import DB |
23 | | -from ZODB.POSException import ConflictError, StorageError |
| 25 | +from ZODB.POSException import ConflictError, StorageError, POSKeyError, \ |
| 26 | + ReadConflictError |
24 | 27 | from ZODB.serialize import referencesf |
25 | 28 | from ZODB.tests.MinPO import MinPO |
26 | 29 | from ZODB.tests.MTStorage import TestThread |
@@ -528,6 +531,109 @@ def checkPackOnlyOneObject(self): |
528 | 531 | eq(pobj.getoid(), oid2) |
529 | 532 | eq(pobj.value, 11) |
530 | 533 |
|
| 534 | + def checkPackVSConnectionGet(self): |
| 535 | + # verify behaviour of Connection.get vs simultaneous pack: |
| 536 | + # |
| 537 | + # For deleted objects, in normal circumstances, get should raise POSKeyError. |
| 538 | + # However when a pack was run simultaneously with packtime going after |
| 539 | + # connection view of the database, for storages that do not implement |
| 540 | + # IMVCCStorage natively, get should raise ReadConflictError instead. |
| 541 | + # |
| 542 | + # IMVCCStorage storages are not affected, since they natively provide |
| 543 | + # isolation, via e.g. RDBMS in case of RelStorage. The isolation |
| 544 | + # property provided by RDBMS guarantees that connection view of the |
| 545 | + # database is not affected by other changes - e.g. pack - until |
| 546 | + # connection's transaction is complete. |
| 547 | + db = DB(self._storage) |
| 548 | + eq = self.assertEqual |
| 549 | + raises = self.assertRaises |
| 550 | + |
| 551 | + # connA is main connection through which database changes are made |
| 552 | + # @at0 - start |
| 553 | + tmA = transaction.TransactionManager() |
| 554 | + connA = db.open(transaction_manager=tmA) |
| 555 | + rootA = connA.root() |
| 556 | + rootA[0] = None |
| 557 | + tmA.commit() |
| 558 | + at0 = rootA._p_serial |
| 559 | + |
| 560 | + # conn0 is "current" db connection that observes database as of @at0 state |
| 561 | + tm0 = transaction.TransactionManager() |
| 562 | + conn0 = db.open(transaction_manager=tm0) |
| 563 | + |
| 564 | + # @at1 - new object is added to database and linked to root |
| 565 | + rootA[0] = objA = MinPO(1) |
| 566 | + tmA.commit() |
| 567 | + oid = objA._p_oid |
| 568 | + at1 = objA._p_serial |
| 569 | + |
| 570 | + # conn1 is "current" db connection that observes database as of @at1 state |
| 571 | + tm1 = transaction.TransactionManager() |
| 572 | + conn1 = db.open(transaction_manager=tm1) |
| 573 | + |
| 574 | + # @at2 - object value is modified |
| 575 | + objA.value = 2 |
| 576 | + tmA.commit() |
| 577 | + at2 = objA._p_serial |
| 578 | + |
| 579 | + # ---- before pack ---- |
| 580 | + |
| 581 | + # conn0.get(oid) -> POSKeyError (as of @at0 object was not yet created) |
| 582 | + errGetNoObject = POSKeyError |
| 583 | + if (not ZODB.interfaces.IMVCCStorage.providedBy(self._storage)) and \ |
| 584 | + (not ZODB.interfaces.IStorageLastPack.providedBy(self._storage)): |
| 585 | + warnings.warn("FIXME %s does not implement lastPack" % |
| 586 | + type(self._storage), DeprecationWarning) |
| 587 | + errGetNoObject = ReadConflictError |
| 588 | + raises(errGetNoObject, conn0.get, oid) |
| 589 | + |
| 590 | + # conn1.get(oid) -> obj(1) |
| 591 | + obj1 = conn1.get(oid) |
| 592 | + eq(obj1._p_oid, oid) |
| 593 | + eq(obj1.value, 1) |
| 594 | + |
| 595 | + # --- after pack to latest db head ---- |
| 596 | + db.pack(time.time()+1) |
| 597 | + |
| 598 | + # IMVCCStorage - as before |
| 599 | + # |
| 600 | + # !IMVCCStorage: |
| 601 | + # conn0.get(oid) -> ReadConflictError |
| 602 | + # conn1.get(oid) -> ReadConflictError |
| 603 | + # |
| 604 | + # ( conn1: the pack removes obj@at1 revision, which results in conn1 |
| 605 | + # finding the object in non-existent/deleted state, traditionally this |
| 606 | + # is reported as ReadConflictError for conn1's transaction to be |
| 607 | + # restarted. |
| 608 | + # |
| 609 | + # conn0: obj@at0 never existed, but after pack@at2 it is |
| 610 | + # indistinguishable whether obj@at0 revision was removed, or it never |
| 611 | + # existed -> ReadConflictError too, similarly to conn1 ) |
| 612 | + conn0.cacheMinimize() |
| 613 | + conn1.cacheMinimize() |
| 614 | + del obj1 |
| 615 | + gc.collect() |
| 616 | + if ZODB.interfaces.IMVCCStorage.providedBy(self._storage): |
| 617 | + raises(POSKeyError, conn0.get, oid) |
| 618 | + obj1 = conn1.get(oid) |
| 619 | + eq(obj1._p_oid, oid) |
| 620 | + eq(obj1.value, 1) |
| 621 | + |
| 622 | + else: |
| 623 | + # !IMVCCStorage |
| 624 | + raises(ReadConflictError, conn0.get, oid) |
| 625 | + raises(ReadConflictError, conn1.get, oid) |
| 626 | + |
| 627 | + # connA stays ok |
| 628 | + connA.cacheMinimize() |
| 629 | + objA_ = connA.get(oid) |
| 630 | + self.assertIs(objA_, objA) |
| 631 | + eq(objA_.value, 2) |
| 632 | + |
| 633 | + # end |
| 634 | + self._sanity_check() |
| 635 | + db.close() |
| 636 | + |
531 | 637 | class PackableStorageWithOptionalGC(PackableStorage): |
532 | 638 |
|
533 | 639 | def checkPackAllRevisionsNoGC(self): |
|
0 commit comments