Merge lp:~pbeaman/akiban-persistit/fix-accumulator-checkpoint-failure into lp:akiban-persistit

Proposed by Peter Beaman
Status: Merged
Approved by: Nathan Williams
Approved revision: 384
Merged at revision: 378
Proposed branch: lp:~pbeaman/akiban-persistit/fix-accumulator-checkpoint-failure
Merge into: lp:akiban-persistit
Diff against target: 367 lines (+209/-17)
8 files modified
src/main/java/com/persistit/Accumulator.java (+31/-11)
src/main/java/com/persistit/CheckpointManager.java (+8/-1)
src/main/java/com/persistit/Persistit.java (+3/-3)
src/main/java/com/persistit/RecoveryManager.java (+1/-1)
src/main/java/com/persistit/Transaction.java (+6/-0)
src/main/java/com/persistit/TransactionPlayer.java (+7/-1)
src/main/java/com/persistit/util/SequencerConstants.java (+10/-0)
src/test/java/com/persistit/Bug1064565Test.java (+143/-0)
To merge this branch: bzr merge lp:~pbeaman/akiban-persistit/fix-accumulator-checkpoint-failure
Reviewer Review Type Date Requested Status
Nathan Williams Approve
Review via email: mp+129243@code.launchpad.net

Description of the change

This proposal fixes https://bugs.launchpad.net/akiban-persistit/+bug/1064565 which is a data-loss bug affecting the state of Accumulator values after system shutdown/restart.

The bug is caused by a subtle race condition in the protocol that determines when Persistit records Accumulator state. The bug mechanism is described in some detail in the bug report.

The central problem is the handling of the _checkpointRef field of the com.persistit.Accumulator.AccumulatorRef inner class. This field is either null or a reference to an Accumulator. It is intended to be null when the Accumulator has not been updated since the last checkpoint, and non-null when there has been an update since the last checkpoint. Checkpoints happen by default every two minutes, so this loose description of the timing is fine for almost all of every two-minute interval. However, the meaning of “since” needs to be carefully defined in the case where a transaction that is performing Accumulator updates becomes concurrent with a checkpoint. That’s when the bug occurs.

The invariant regarding timing is this: if some committed transaction with commit timestamp tc has updated an Accumulator, and if the most recent checkpoint has timestamp ts0 then the _checkpointRef field must be non-null whenever there is any transaction such that tc > ts0. The converse is not true; it is permissible for _checkpointRef to be non-null even if there are no qualifying updates. The result in that case is that the Accumulator state is recorded redundantly, but not incorrectly.

The reason for this invariant is as follows. Checkpoint C0 will record the snapshot value of the Accumulator as of its start timestamp ts0. By definition of SI, updates committed after ts0 will not be part of the checkpoint even if the transaction that created them started before ts0. In the event C0 is the very last checkpoint recorded before recovery, the state is valid because those subsequent updates would be replayed from the journal during recovery. However, if one more checkpoint C1 is written (as it is under normal shutdown) with start timestamp ts1 > tc, recovery will ignore the transactions in the journal (by definition of Checkpoint). Therefore C1 must record the updates committed at tc in order for correct recovery after C1 has been written to the journal. It is the value of _checkpointRef that determines this behavior.

The bug occurred because this invariant was violated. The following is an informal proof that the proposed changes enforce the invariant.

Changes:

AccumulatorRef now includes a new AtomicLong field called _latestUpdate in which the commit timestamp of the most-recently-started transaction performing an update on the Accumulator is stored. This field is modified using a lock-free CAS loop in AccumulatorRef#checkpointNeeded(…).

The AccumulatorRef#takeCheckpointRef() method has been modified to receive the start timestamp of the checkpoint transaction that is saving Accumulator state. This method sets _checkpointRef to null only if the checkpoint timestamp is greater than the then-current value of _latestUpdate.

The checkpointNeeded(timestamp) method is called from a different place than before. It is now called during the processing of Transaction#commit(…) at a time when the ultimate commit timestamp of the transaction has been determined.

Other minor change:

The Accumulator _baseValue field is now marked volatile because it is set during recovery by one thread and may first be read by a different thread. There is no race condition requiring synchronization, but the Java memory model would permit the second thread to receive stale data.

Informal Proof:

Two threads C and T are executing concurrently; C is calling takeCheckpointRef(ts) (while performing a checkpoint), and T is calling checkpointNeeded(tc) (while committing a transaction). Once both calls are finished we need to prove that under all execution schedules, the value of _checkpointRef is non-null if tc > ts.

(Note that both of these methods are lock-free because they are called frequently.)

These methods modify two shared variables, _checkpointRef and _latestUpdate – for brevity call these R and L, respectively.

T updates L then sets R.
C reads L, possibly clears R, reads L and possibly sets R again.

By definition of Java, the operations “update”, “set”, “read” and “clear” are atomic. We’ll use the following abbreviations:

TuL – T updates L
TsR – T sets R
CrL1 – C reads L the first time
CcR – C clears R
CrL2 – C reads L the second time
CsR – C sets R

Clearly all the execution schedules in which T’s operations either precede or follow C’s operations (i.e., TuL, TsR, CrL1, CcR… and CrL1, CcR…, TuL, TsR) are safe.

All schedules in which TuL precedes CrL1 are safe because C will never clear R. The following are the two such possible executions:

TuL, TsR, CrL1
TuL, CrL1, TsR

Now consider schedules in which TuL follows CrL1 but precedes CcR.

CrL1, TuL, TsR, CcR, CrL2, CsR

In this schedule C sees an old value of L and acts by clearing R. In this case the second read CrL2 sees the updated value of L and restores the value of C, leaving C correctly non-null. (This is the case that motivates the CrL2, CsR sequence in takeCheckpointRef.)

CrL1, TuL, CcR, TsR, CrL2, CsR

In this schedule TsR occurs after CcR so the value of C is already non-null. The final execution of CsR simply redundantly sets R. Same analysis applies to the following cases:

CrL1, TuL, CcR, CrL2, TsR, CsR
CrL1, TuL, CcR, CrL2, CsR, TsR

Finally, all schedules in which TuL follows CcR are all safe because in all such cases TsR must follow CsR, e.g.:

CrL1, CcR, TuL, TsR, CrL2, CsR
CrL1, CcR, TuL, CrL2, TsR, CsR
CrL1, CcR, TuL, CrL2, CsR, TsR

To post a comment you must log in.
Revision history for this message
Nathan Williams (nwilliams) wrote :

With regards to takeCheckpointRef and checkpointNeeded, the description is very complete and the code looks consistent with it. I believe it is correct as-is.

I am a little wary of diff line 81 though. Unconditionally calling checkpointNeeded() with 0 requires that checkpointNeeded always sets the ref. It does today, but I was about to suggest diff line 47 could be return instead, without loss of correctness, to prevent one case of spurious saves.

It looks like there is exactly 1 caller of updateBaseValue() and a timestamp is readily available. Is there a reason we shouldn't pass the ts down so that we are always dealing with real ones in checkpointNeeded?

Minor though and otherwise looks good!

review: Needs Information
Revision history for this message
Peter Beaman (pbeaman) wrote :

I think my explanation of the updateBaseValue call is a little off. I'll fix the Javadoc. updateBaseValue is called from RecoveryManager.DefaultRecoveryListener to apply a change from a committed transaction that has not yet been checkpointed. So in fact the setting of _checkpointRef in checkpointNeeded will occur precisely when a checkpoint is in fact needed, and there should be no redundant checkpointing. Re 0 vs. the commit timestamp of the transaction: the choice is immaterial; the intent is to set _checkpointRef so that the first checkpoint cycle of the new epoch will checkpoint the accumulator. If updateBaseValue were engaged in a race with the checkpoint manager then the distinction would matter, but checkpoint manager starts after recovery is done. But upon reflection, given that someday someone might change that, I'll change it to pass commit timestamp.

Re return vs. break at +47: I think that might be okay, but it complicates the "proof." Need to think about that one. As is, I believe the code is correct, but the change could eliminate a redundant set operation on volatile _checkpointRef and that might be a tiny help with highly concurrent transactions.

Revision history for this message
Nathan Williams (nwilliams) wrote :

To be clear, I wasn't worried about a race for updateBaseValue. I was worried that it was passing zero and expecting that to *always* set the ref. This is purely for future proofing changes to checkpointNeeded and reducing "magic" values. Another option would be a checkpointNeeded that takes no timestamp and just unconditionally sets the ref.

383. By Peter Beaman

Review comments: pass commitTimestamp to updateBaseValue.

384. By Peter Beaman

Review comment: return in diff +47

Revision history for this message
Peter Beaman (pbeaman) wrote :

Agree about the magic value. Changed it so that updateBaseValue passes the actual commitTimestamp. Also changed break to return in +47. I think that's fine. The important thing is to ensure that _checkpointRef is always set when it needs to be, and I think that property is preserved.

Revision history for this message
Nathan Williams (nwilliams) wrote :

Thanks for the tweak! I think this is good to go now.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/main/java/com/persistit/Accumulator.java'
2--- src/main/java/com/persistit/Accumulator.java 2012-08-24 13:57:19 +0000
3+++ src/main/java/com/persistit/Accumulator.java 2012-10-11 20:43:21 +0000
4@@ -122,7 +122,7 @@
5 /*
6 * Check-pointed value read during recovery.
7 */
8- private long _baseValue;
9+ private volatile long _baseValue;
10
11 /*
12 * Snapshot value at the most recent checkpoint
13@@ -366,23 +366,37 @@
14 */
15 final static class AccumulatorRef {
16 final WeakReference<Accumulator> _weakRef;
17+ final AtomicLong _latestUpdate = new AtomicLong();
18 volatile Accumulator _checkpointRef;
19
20 AccumulatorRef(final Accumulator acc) {
21 _weakRef = new WeakReference<Accumulator>(acc);
22- _checkpointRef = acc;
23 }
24
25- Accumulator takeCheckpointRef() {
26+ Accumulator takeCheckpointRef(final long timestamp) {
27 final Accumulator result = _checkpointRef;
28- _checkpointRef = null;
29+
30+ if (timestamp > _latestUpdate.get()) {
31+ _checkpointRef = null;
32+ if (timestamp <= _latestUpdate.get()) {
33+ _checkpointRef = result;
34+ }
35+ }
36+
37 return result;
38 }
39
40- void checkpointNeeded(final Accumulator acc) {
41- if (_checkpointRef == null) {
42- _checkpointRef = acc;
43+ void checkpointNeeded(final Accumulator acc, final long timestamp) {
44+ while (true) {
45+ final long latest = _latestUpdate.get();
46+ if (latest > timestamp) {
47+ return;
48+ }
49+ if (_latestUpdate.compareAndSet(latest, timestamp)) {
50+ break;
51+ }
52 }
53+ _checkpointRef = acc;
54 }
55
56 boolean isLive() {
57@@ -448,8 +462,8 @@
58 return _accumulatorRef;
59 }
60
61- void checkpointNeeded() {
62- _accumulatorRef.checkpointNeeded(this);
63+ void checkpointNeeded(final long timestamp) {
64+ _accumulatorRef.checkpointNeeded(this, timestamp);
65 }
66
67 long getBucketValue(final int hashIndex) {
68@@ -563,9 +577,16 @@
69 *
70 * @param value
71 */
72- void updateBaseValue(final long value) {
73+ void updateBaseValue(final long value, final long commitTimestamp) {
74 _baseValue = applyValue(_baseValue, value);
75 _liveValue.set(_baseValue);
76+ /*
77+ * This method is called during recovery processing to handle a delta
78+ * operation that was part of a transaction that committed after the
79+ * keystone checkpoint. That update requires the accumulator to be saved
80+ * on the next checkpoint.
81+ */
82+ checkpointNeeded(commitTimestamp);
83 }
84
85 /**
86@@ -619,7 +640,6 @@
87 */
88 final long selectedValue = selectValue(value, updated);
89 _transactionIndex.addOrCombineDelta(status, this, step, selectedValue);
90- checkpointNeeded();
91 return updated;
92 }
93
94
95=== modified file 'src/main/java/com/persistit/CheckpointManager.java'
96--- src/main/java/com/persistit/CheckpointManager.java 2012-10-04 20:40:40 +0000
97+++ src/main/java/com/persistit/CheckpointManager.java 2012-10-11 20:43:21 +0000
98@@ -15,6 +15,8 @@
99
100 package com.persistit;
101
102+import static com.persistit.util.SequencerConstants.ACCUMULATOR_CHECKPOINT_A;
103+import static com.persistit.util.ThreadSequencer.sequence;
104 import static com.persistit.util.Util.NS_PER_S;
105
106 import java.text.SimpleDateFormat;
107@@ -246,7 +248,12 @@
108 txn.beginCheckpoint();
109 try {
110 _persistit.flushTransactions(txn.getStartTimestamp());
111- final List<Accumulator> accumulators = _persistit.getCheckpointAccumulators();
112+ /*
113+ * Test only: block here while Accumulator update occurs
114+ */
115+ sequence(ACCUMULATOR_CHECKPOINT_A);
116+
117+ final List<Accumulator> accumulators = _persistit.takeCheckpointAccumulators(txn.getStartTimestamp());
118 _persistit.getTransactionIndex().checkpointAccumulatorSnapshots(txn.getStartTimestamp(), accumulators);
119 Accumulator.saveAccumulatorCheckpointValues(accumulators);
120 txn.commit(CommitPolicy.HARD);
121
122=== modified file 'src/main/java/com/persistit/Persistit.java'
123--- src/main/java/com/persistit/Persistit.java 2012-10-08 19:10:49 +0000
124+++ src/main/java/com/persistit/Persistit.java 2012-10-11 20:43:21 +0000
125@@ -2419,7 +2419,7 @@
126 }
127 }
128 }
129- if ((checkpointCount % ACCUMULATOR_CHECKPOINT_THRESHOLD) == 0) {
130+ if (checkpointCount > 0 && (checkpointCount % ACCUMULATOR_CHECKPOINT_THRESHOLD) == 0) {
131 try {
132 _checkpointManager.createCheckpoint();
133 } catch (final PersistitException e) {
134@@ -2441,7 +2441,7 @@
135 }
136 }
137
138- List<Accumulator> getCheckpointAccumulators() {
139+ List<Accumulator> takeCheckpointAccumulators(final long timestamp) {
140 final List<Accumulator> result = new ArrayList<Accumulator>();
141 synchronized (_accumulators) {
142 for (final Iterator<AccumulatorRef> refIterator = _accumulators.iterator(); refIterator.hasNext();) {
143@@ -2449,7 +2449,7 @@
144 if (!ref.isLive()) {
145 refIterator.remove();
146 }
147- final Accumulator acc = ref.takeCheckpointRef();
148+ final Accumulator acc = ref.takeCheckpointRef(timestamp);
149 if (acc != null) {
150 result.add(acc);
151 }
152
153=== modified file 'src/main/java/com/persistit/RecoveryManager.java'
154--- src/main/java/com/persistit/RecoveryManager.java 2012-10-03 16:04:16 +0000
155+++ src/main/java/com/persistit/RecoveryManager.java 2012-10-11 20:43:21 +0000
156@@ -246,7 +246,7 @@
157 final int accumulatorTypeOrdinal, final long value) throws PersistitException {
158 final Accumulator.Type type = Accumulator.Type.values()[accumulatorTypeOrdinal];
159 final Accumulator accumulator = tree.getAccumulator(type, index);
160- accumulator.updateBaseValue(value);
161+ accumulator.updateBaseValue(value, timestamp);
162 }
163
164 @Override
165
166=== modified file 'src/main/java/com/persistit/Transaction.java'
167--- src/main/java/com/persistit/Transaction.java 2012-10-05 19:37:58 +0000
168+++ src/main/java/com/persistit/Transaction.java 2012-10-11 20:43:21 +0000
169@@ -854,6 +854,12 @@
170 _commitTimestamp = _persistit.getTimestampAllocator().updateTimestamp();
171 sequence(COMMIT_FLUSH_C);
172 long flushedTimetimestamp = 0;
173+
174+ for (Delta delta = _transactionStatus.getDelta(); delta != null; delta = delta.getNext()) {
175+ final Accumulator acc = delta.getAccumulator();
176+ acc.checkpointNeeded(_commitTimestamp);
177+ }
178+
179 boolean committed = false;
180 try {
181
182
183=== modified file 'src/main/java/com/persistit/TransactionPlayer.java'
184--- src/main/java/com/persistit/TransactionPlayer.java 2012-10-03 16:04:16 +0000
185+++ src/main/java/com/persistit/TransactionPlayer.java 2012-10-11 20:43:21 +0000
186@@ -225,7 +225,13 @@
187
188 case D0.TYPE: {
189 final Exchange exchange = getExchange(D0.getTreeHandle(bb), address, startTimestamp);
190- listener.delta(address, startTimestamp, exchange.getTree(), D0.getIndex(bb),
191+ /*
192+ * Note that the commitTimestamp, not startTimestamp is
193+ * passed to the delta method. The
194+ * Accumulator#updateBaseValue method needs the
195+ * commitTimestamp.
196+ */
197+ listener.delta(address, commitTimestamp, exchange.getTree(), D0.getIndex(bb),
198 D0.getAccumulatorTypeOrdinal(bb), 1);
199 appliedUpdates.incrementAndGet();
200 break;
201
202=== modified file 'src/main/java/com/persistit/util/SequencerConstants.java'
203--- src/main/java/com/persistit/util/SequencerConstants.java 2012-09-28 21:39:44 +0000
204+++ src/main/java/com/persistit/util/SequencerConstants.java 2012-10-11 20:43:21 +0000
205@@ -103,4 +103,14 @@
206 array(DEALLOCATE_CHAIN_B), array(DEALLOCATE_CHAIN_A, DEALLOCATE_CHAIN_C),
207 array(DEALLOCATE_CHAIN_A, DEALLOCATE_CHAIN_C) };
208
209+ /*
210+ * Used in testing delete/deallocate sequence in Bug1022567Test
211+ */
212+ int ACCUMULATOR_CHECKPOINT_A = allocate("ACCUMULATOR_CHECKPOINT_A");
213+ int ACCUMULATOR_CHECKPOINT_B = allocate("ACCUMULATOR_CHECKPOINT_B");
214+ int ACCUMULATOR_CHECKPOINT_C = allocate("ACCUMULATOR_CHECKPOINT_C");
215+ int[][] ACCUMULATOR_CHECKPOINT_SCHEDULED = new int[][] { array(ACCUMULATOR_CHECKPOINT_A, ACCUMULATOR_CHECKPOINT_B),
216+ array(ACCUMULATOR_CHECKPOINT_B), array(ACCUMULATOR_CHECKPOINT_A, ACCUMULATOR_CHECKPOINT_C),
217+ array(ACCUMULATOR_CHECKPOINT_A, ACCUMULATOR_CHECKPOINT_C) };
218+
219 }
220
221=== added file 'src/test/java/com/persistit/Bug1064565Test.java'
222--- src/test/java/com/persistit/Bug1064565Test.java 1970-01-01 00:00:00 +0000
223+++ src/test/java/com/persistit/Bug1064565Test.java 2012-10-11 20:43:21 +0000
224@@ -0,0 +1,143 @@
225+/**
226+ * Copyright © 2012 Akiban Technologies, Inc. All rights reserved.
227+ *
228+ * This program and the accompanying materials are made available
229+ * under the terms of the Eclipse Public License v1.0 which
230+ * accompanies this distribution, and is available at
231+ * http://www.eclipse.org/legal/epl-v10.html
232+ *
233+ * This program may also be available under different license terms.
234+ * For more information, see www.akiban.com or contact licensing@akiban.com.
235+ *
236+ * Contributors:
237+ * Akiban Technologies, Inc.
238+ */
239+
240+package com.persistit;
241+
242+import static com.persistit.unit.UnitTestProperties.VOLUME_NAME;
243+import static com.persistit.util.SequencerConstants.ACCUMULATOR_CHECKPOINT_A;
244+import static com.persistit.util.SequencerConstants.ACCUMULATOR_CHECKPOINT_B;
245+import static com.persistit.util.SequencerConstants.ACCUMULATOR_CHECKPOINT_C;
246+import static com.persistit.util.SequencerConstants.ACCUMULATOR_CHECKPOINT_SCHEDULED;
247+import static com.persistit.util.ThreadSequencer.addSchedules;
248+import static com.persistit.util.ThreadSequencer.enableSequencer;
249+import static com.persistit.util.ThreadSequencer.sequence;
250+import static com.persistit.util.ThreadSequencer.setCondition;
251+import static org.junit.Assert.assertEquals;
252+
253+import java.util.concurrent.atomic.AtomicBoolean;
254+
255+import org.junit.Test;
256+
257+import com.persistit.exception.PersistitException;
258+import com.persistit.util.ThreadSequencer.Condition;
259+
260+/**
261+ * https://bugs.launchpad.net/akiban-persistit/+bug/1064565
262+ *
263+ * The state of an Accumulator is sometimes incorrect after shutting down and
264+ * restarting Persistit and as a result an application can read a count or value
265+ * that is inconsistent with the history of committed transactions.
266+ *
267+ * The bug mechanism is a race between the CheckpointManager#createCheckpoint
268+ * method and the Accumulator#update method in which an update which occurs in a
269+ * transaction that starts immediately after the checkpoint begins its
270+ * transaction can be lost. The probability of failure is low but may be
271+ * increased by intense I/O activity.
272+ *
273+ * This is a data loss error and is therefore critical.
274+ */
275+
276+public class Bug1064565Test extends PersistitUnitTestCase {
277+
278+ private final static String TREE_NAME = "Bug1064565Test";
279+
280+ private Exchange getExchange() throws PersistitException {
281+ return _persistit.getExchange(VOLUME_NAME, TREE_NAME, true);
282+ }
283+
284+ @Test
285+ public void accumulatorRace() throws Exception {
286+ enableSequencer(false);
287+ addSchedules(ACCUMULATOR_CHECKPOINT_SCHEDULED);
288+ final AtomicBoolean once = new AtomicBoolean(true);
289+ setCondition(ACCUMULATOR_CHECKPOINT_A, new Condition() {
290+ @Override
291+ public boolean enabled() {
292+ return once.getAndSet(false);
293+ }
294+ });
295+
296+ Exchange exchange = getExchange();
297+ Transaction txn = exchange.getTransaction();
298+ final Thread t = new Thread(new Runnable() {
299+ @Override
300+ public void run() {
301+ try {
302+ _persistit.checkpoint();
303+ } catch (final PersistitException e) {
304+ throw new RuntimeException(e);
305+ }
306+ }
307+ });
308+ t.start();
309+
310+ txn.begin();
311+ Accumulator acc = exchange.getTree().getAccumulator(Accumulator.Type.SUM, 0);
312+ acc.update(42, txn);
313+ sequence(ACCUMULATOR_CHECKPOINT_B);
314+ txn.commit();
315+ txn.end();
316+ sequence(ACCUMULATOR_CHECKPOINT_C);
317+
318+ _persistit.close();
319+
320+ final Configuration config = _persistit.getConfiguration();
321+ _persistit = new Persistit();
322+ _persistit.initialize(config);
323+
324+ exchange = getExchange();
325+ txn = exchange.getTransaction();
326+ txn.begin();
327+ acc = exchange.getTree().getAccumulator(Accumulator.Type.SUM, 0);
328+ assertEquals("Accumulator state should have been checkpointed", 42, acc.getSnapshotValue(txn));
329+ txn.commit();
330+ txn.end();
331+
332+ _persistit.checkpoint();
333+ _persistit.checkpoint();
334+ _persistit.checkpoint();
335+ }
336+
337+ /**
338+ * ThreadSequencer is not even needed: this sequence shows how setting
339+ * checkpointNeeded inside of the main transaction is not correctly
340+ * sequenced against the checkpoint.
341+ */
342+ @Test
343+ public void nathansVersion() throws Exception {
344+ Exchange exchange = getExchange();
345+ Transaction txn = exchange.getTransaction();
346+ txn.begin();
347+ Accumulator acc = exchange.getTree().getAccumulator(Accumulator.Type.SUM, 0);
348+ acc.update(42, txn);
349+ _persistit.checkpoint();
350+ txn.commit();
351+ txn.end();
352+ _persistit.copyBackPages();
353+ final Configuration config = _persistit.getConfiguration();
354+ _persistit.close();
355+ _persistit = new Persistit();
356+ _persistit.initialize(config);
357+
358+ exchange = getExchange();
359+ txn = exchange.getTransaction();
360+ txn.begin();
361+ acc = exchange.getTree().getAccumulator(Accumulator.Type.SUM, 0);
362+ assertEquals("Accumulator state should have been checkpointed", 42, acc.getSnapshotValue(txn));
363+ txn.commit();
364+ txn.end();
365+
366+ }
367+}

Subscribers

People subscribed via source and target branches