@@ -197,6 +197,13 @@ protected static int defaultSlotSize()
197197 */
198198 protected boolean isProcessingItems ;
199199
200+ /*
201+ * First failure of item processing. A failed store leaves partially serialized data (unprocessed items,
202+ * a half-written entity, commit listeners of the failed store), so the storer must not store or commit
203+ * until it is cleared. Guarded by objectRegistryMonitor.
204+ */
205+ private Throwable storeFailure ;
206+
200207 /*
201208 * item hashing structures get initialized lazily for the following reasons:
202209 * - the storer instance can commit (be cleared) and be reinitialized multiple times.
@@ -428,6 +435,8 @@ protected void internalInitialize(final int hashLength)
428435 this .hashSlots = new Item [hashLength ];
429436 this .hashRange = hashLength - 1 ;
430437
438+ this .storeFailure = null ;
439+
431440 // initializing/clearing item chain
432441 (this .tail = this .head ).next = null ;
433442
@@ -603,6 +612,11 @@ protected long internalStore(final Object root)
603612 * - handle all registered graph objects recursively (but transformed to an iteration).
604613 * Note that this is NOT the same as apply, which does NOT store if the instance is already registry-known.
605614 */
615+ if (!this .isProcessingItems )
616+ {
617+ this .validateNoFailedStore ();
618+ }
619+
606620 long rootOid ;
607621 if (Swizzling .isFoundId (rootOid = this .lookupOid (root )))
608622 {
@@ -615,15 +629,7 @@ protected long internalStore(final Object root)
615629 // repeatedly calling #store to add an instance to the item chain is fine, but processing may only happen once.
616630 if (!this .isProcessingItems )
617631 {
618- try
619- {
620- this .isProcessingItems = true ;
621- this .processItems ();
622- }
623- finally
624- {
625- this .isProcessingItems = false ;
626- }
632+ this .processItemsOutermost ();
627633 }
628634
629635 return rootOid ;
@@ -666,6 +672,65 @@ protected final void storeItem(final Item item)
666672 item .typeHandler .store (chunk , item .instance , item .oid , this );
667673 }
668674
675+ /**
676+ * Processes the item chain as the outermost store call and records the first failure.
677+ */
678+ protected final void processItemsOutermost ()
679+ {
680+ try
681+ {
682+ this .isProcessingItems = true ;
683+ this .processItems ();
684+ }
685+ catch (final Throwable t )
686+ {
687+ synchronized (this .objectRegistryMonitor )
688+ {
689+ if (this .storeFailure == null )
690+ {
691+ this .storeFailure = t ;
692+ }
693+ }
694+ throw t ;
695+ }
696+ finally
697+ {
698+ this .isProcessingItems = false ;
699+ }
700+ }
701+
702+ /**
703+ * Rejects storing and committing after a failed store: committing would persist the partially
704+ * serialized graph. Only {@link #clear()} resets the storer.
705+ * <p>
706+ * Must be called under {@code objectRegistryMonitor} and not during item processing.
707+ */
708+ protected final void synchValidateNoFailedStore ()
709+ {
710+ if (this .storeFailure != null )
711+ {
712+ throw new PersistenceException (
713+ "A store of this storer failed and left partially serialized data."
714+ + " Committing it would persist an inconsistent object graph."
715+ + " The storer must be cleared before it can be used again." ,
716+ this .storeFailure
717+ );
718+ }
719+ }
720+
721+ protected final Throwable synchStoreFailure ()
722+ {
723+ return this .storeFailure ;
724+ }
725+
726+ protected final void validateNoFailedStore ()
727+ {
728+ synchronized (this .objectRegistryMonitor )
729+ {
730+ this .synchValidateNoFailedStore ();
731+ }
732+ }
733+
669734 @ Override
670735 public final long store (final Object root )
671736 {
@@ -797,6 +862,9 @@ public Object commit()
797862 LazyArg (this ::size ) // use lazy here, #size() locks
798863 );
799864
865+ // before anything is written, merged or notified: the storer keeps its state until cleared
866+ this .validateNoFailedStore ();
867+
800868 // isEmpty locks internally
801869 if (!this .isEmpty ())
802870 {
@@ -1455,6 +1523,11 @@ protected final long internalStore(final Object root, final long objectId)
14551523 * - handle all registered graph objects recursively (but transformed to an iteration).
14561524 * Note that this is NOT the same as apply, which does NOT store if the instance is already registry-known.
14571525 */
1526+ if (!this .isProcessingItems )
1527+ {
1528+ this .validateNoFailedStore ();
1529+ }
1530+
14581531 final long rootOid ;
14591532 if (Swizzling .isFoundId (rootOid = this .lookupOid (root )))
14601533 {
@@ -1469,15 +1542,7 @@ protected final long internalStore(final Object root, final long objectId)
14691542 // repeatedly calling #store to add an instance to the item chain is fine, but processing may only happen once.
14701543 if (!this .isProcessingItems )
14711544 {
1472- try
1473- {
1474- this .isProcessingItems = true ;
1475- this .processItems ();
1476- }
1477- finally
1478- {
1479- this .isProcessingItems = false ;
1480- }
1545+ this .processItemsOutermost ();
14811546 }
14821547
14831548 return objectId ;
@@ -1490,6 +1555,11 @@ public void forceRootStore(final PersistenceRoots pendingStoreRoot) {
14901555
14911556 logger .debug ("Storing updated root" );
14921557
1558+ if (!this .isProcessingItems )
1559+ {
1560+ this .validateNoFailedStore ();
1561+ }
1562+
14931563 long rootOid ;
14941564 if (Swizzling .isFoundId (rootOid = this .lookupOid (pendingStoreRoot )))
14951565 {
@@ -1502,15 +1572,7 @@ public void forceRootStore(final PersistenceRoots pendingStoreRoot) {
15021572
15031573 if (!this .isProcessingItems )
15041574 {
1505- try
1506- {
1507- this .isProcessingItems = true ;
1508- this .processItems ();
1509- }
1510- finally
1511- {
1512- this .isProcessingItems = false ;
1513- }
1575+ this .processItemsOutermost ();
15141576 }
15151577 }
15161578
@@ -1681,6 +1743,7 @@ public final class Batching extends Default implements BatchStorer
16811743 private final BatchStorer .Controller controller ;
16821744 private final BackgroundFlusher flusher ;
16831745 private long pendingSinceNanos ;
1746+ private boolean failureReported ;
16841747 private volatile boolean closed ;
16851748
16861749 /*
@@ -1761,19 +1824,15 @@ public long store(final Object instance, final long objectId)
17611824 {
17621825 throw new IllegalStateException ("BatchStorer is already closed." );
17631826 }
1827+ if (!this .isProcessingItems )
1828+ {
1829+ this .validateNoFailedStore ();
1830+ }
17641831 this .registerGuaranteed (objectId , instance , null );
17651832
17661833 if (!this .isProcessingItems )
17671834 {
1768- try
1769- {
1770- this .isProcessingItems = true ;
1771- super .processItems ();
1772- }
1773- finally
1774- {
1775- this .isProcessingItems = false ;
1776- }
1835+ this .processItemsOutermost ();
17771836 }
17781837 }
17791838
@@ -1850,6 +1909,10 @@ protected long internalStore(final Object root)
18501909 {
18511910 throw new IllegalStateException ("BatchStorer is already closed." );
18521911 }
1912+ if (!this .isProcessingItems )
1913+ {
1914+ this .validateNoFailedStore ();
1915+ }
18531916
18541917 final long localOid = this .lookupOid (root );
18551918
@@ -1868,15 +1931,7 @@ protected long internalStore(final Object root)
18681931
18691932 if (!this .isProcessingItems )
18701933 {
1871- try
1872- {
1873- this .isProcessingItems = true ;
1874- super .processItems ();
1875- }
1876- finally
1877- {
1878- this .isProcessingItems = false ;
1879- }
1934+ this .processItemsOutermost ();
18801935 }
18811936 }
18821937
@@ -1908,10 +1963,33 @@ protected void processItems()
19081963 @ Override
19091964 public void clear ()
19101965 {
1911- synchronized (this .objectRegistryMonitor )
1966+ // commitLock: an in-flight store of another thread must not continue into the cleared chain and chunks
1967+ synchronized (this .commitLock )
1968+ {
1969+ synchronized (this .objectRegistryMonitor )
1970+ {
1971+ super .clear ();
1972+ this .pendingSinceNanos = 0L ;
1973+ this .failureReported = false ;
1974+ }
1975+ }
1976+ }
1977+
1978+ @ Override
1979+ public PersistenceStorer reinitialize ()
1980+ {
1981+ synchronized (this .commitLock )
19121982 {
1913- super .clear ();
1914- this .pendingSinceNanos = 0L ;
1983+ return super .reinitialize ();
1984+ }
1985+ }
1986+
1987+ @ Override
1988+ public PersistenceStorer reinitialize (final long initialCapacity )
1989+ {
1990+ synchronized (this .commitLock )
1991+ {
1992+ return super .reinitialize (initialCapacity );
19151993 }
19161994 }
19171995
@@ -1995,6 +2073,18 @@ private void optFlush()
19952073 return ;
19962074 }
19972075
2076+ // a failed store blocks committing until clear(): report it once instead of failing on every tick
2077+ final Throwable storeFailure = this .synchStoreFailure ();
2078+ if (storeFailure != null )
2079+ {
2080+ if (!this .failureReported )
2081+ {
2082+ this .failureReported = true ;
2083+ logger .error ("Batch not flushed: a store failed, the pending data is kept until clear()" , storeFailure );
2084+ }
2085+ return ;
2086+ }
2087+
19982088 final long nowNanos = System .nanoTime ();
19992089 if (this .pendingSinceNanos == 0L )
20002090 {
0 commit comments