Skip to content

Commit cc7c6df

Browse files
authored
Reject storing and committing after a failed store until clear() (#322)
* Reject storing and committing after a failed store until clear() A store() that threw left its partially serialized graph in the storer; a later commit, or a BatchStorer flush or close, persisted it: dangling references, a half-written entity and never-written objects registered as stored. The storage could not be restarted. The storer now records the first failure of item processing and rejects further stores and commits until clear(). A BatchStorer reports a failed batch once and does not flush it. BatchStorer.clear() and reinitialize() wait for an in-flight store. Serializer and TypedSerializer clear their internal storer when storing or committing fails. * Document the failed-store contract of Storer and BatchStorer * Document that a closed BatchStorer cannot recover a failed batch
1 parent 9c955b0 commit cc7c6df

5 files changed

Lines changed: 187 additions & 54 deletions

File tree

‎persistence/binary/src/main/java/org/eclipse/serializer/persistence/binary/types/BinaryStorer.java‎

Lines changed: 138 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -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
{

‎persistence/persistence/src/main/java/org/eclipse/serializer/persistence/types/BatchStorer.java‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@
3636
* {@code BatchStorer} implements {@link AutoCloseable}. On close, it flushes any remaining
3737
* pending data and releases resources.
3838
* <p>
39+
* If a store call fails, the batch holds incomplete data and is not flushed: further store calls,
40+
* {@link #flush()}, {@link #commit()} and {@link #close()} throw a
41+
* {@link org.eclipse.serializer.persistence.exceptions.PersistenceException}, and the background
42+
* flush skips the batch. This affects the whole batch, including data stored by other threads.
43+
* {@link #clear()} discards the pending data and makes the {@code BatchStorer} usable again, unless
44+
* it was already closed: {@link #close()} releases its resources even when the final flush is
45+
* rejected, so a failed batch must be cleared before closing it to keep using it.
46+
* <p>
3947
* Usage:
4048
* <pre>
4149
* try (BatchStorer storer = storageManager.batchStorerBuilder()

‎persistence/persistence/src/main/java/org/eclipse/serializer/persistence/types/Storer.java‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ public interface Storer extends PersistenceStoring
5252
* <p>
5353
* The method may be called at most once on a fresh storer state. To reuse the storer, call
5454
* {@link #clear()} or {@link #reinitialize()} afterwards.
55+
* <p>
56+
* If a {@code store(...)} call on this storer failed, the accumulated data is incomplete. In that
57+
* case {@code commit()} persists nothing and throws a
58+
* {@link org.eclipse.serializer.persistence.exceptions.PersistenceException} with the store
59+
* failure as its cause, until {@link #clear()} discards the accumulated data.
5560
*
5661
* @return implementation-specific status information about the commit, or {@code null} if the
5762
* implementation does not provide any.
@@ -69,6 +74,8 @@ public interface Storer extends PersistenceStoring
6974
* This rolls back uncommitted in-memory state only; data already committed by a previous
7075
* {@link #commit()} call is unaffected. After {@code clear()} the storer is ready to start a new
7176
* collection cycle.
77+
* <p>
78+
* After a failed {@code store(...)} call, {@code clear()} is the only way to use the storer again.
7279
*
7380
* @see #commit()
7481
* @see #reinitialize()
@@ -264,6 +271,12 @@ public default boolean isEmpty()
264271
* {@link PersistenceObjectRegistry} and nothing is written to the underlying storage layer.
265272
* Calling {@code store(...)} on the same instance multiple times before {@code commit()} has no
266273
* additional effect.
274+
* <p>
275+
* If this call fails, for example because the graph contains an instance that cannot be persisted,
276+
* the data accumulated by this storer is incomplete. Further {@code store(...)} and
277+
* {@link #commit()} calls then throw a
278+
* {@link org.eclipse.serializer.persistence.exceptions.PersistenceException} until
279+
* {@link #clear()} is called.
267280
*
268281
* @see #commit()
269282
* @see #skip(Object)

‎serializer/src/main/java/org/eclipse/serializer/Serializer.java‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,8 +207,17 @@ public static class Default<M> implements Serializer<M>
207207
@Override
208208
public synchronized M serialize(final Object object)
209209
{
210-
this.storer.store(object);
211-
this.storer.commit();
210+
try
211+
{
212+
this.storer.store(object);
213+
this.storer.commit();
214+
}
215+
catch(final Throwable t)
216+
{
217+
// the internal storer would carry the partial graph into the next serialize() call
218+
this.storer.clear();
219+
throw t;
220+
}
212221
return this.toMedium.apply(this.output);
213222
}
214223

0 commit comments

Comments
 (0)