View Javadoc

1   /**
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  
19  package org.apache.hadoop.hbase.procedure2.store.wal;
20  
21  import com.google.protobuf.InvalidProtocolBufferException;
22  
23  import java.io.IOException;
24  
25  import org.apache.commons.logging.Log;
26  import org.apache.commons.logging.LogFactory;
27  import org.apache.hadoop.fs.FSDataInputStream;
28  import org.apache.hadoop.hbase.ProcedureInfo;
29  import org.apache.hadoop.hbase.classification.InterfaceAudience;
30  import org.apache.hadoop.hbase.classification.InterfaceStability;
31  import org.apache.hadoop.hbase.procedure2.Procedure;
32  import org.apache.hadoop.hbase.procedure2.store.ProcedureStore.ProcedureIterator;
33  import org.apache.hadoop.hbase.procedure2.store.ProcedureStoreTracker;
34  import org.apache.hadoop.hbase.protobuf.generated.ProcedureProtos;
35  import org.apache.hadoop.hbase.protobuf.generated.ProcedureProtos.ProcedureWALEntry;
36  
37  /**
38   * Helper class that loads the procedures stored in a WAL
39   */
40  @InterfaceAudience.Private
41  @InterfaceStability.Evolving
42  public class ProcedureWALFormatReader {
43    private static final Log LOG = LogFactory.getLog(ProcedureWALFormatReader.class);
44  
45    // ==============================================================================================
46    //  We read the WALs in reverse order. from the newest to the oldest.
47    //  We have different entry types:
48    //   - INIT: Procedure submitted by the user (also known as 'root procedure')
49    //   - INSERT: Children added to the procedure <parentId>:[<childId>, ...]
50    //   - UPDATE: The specified procedure was updated
51    //   - DELETE: The procedure was removed (completed/rolledback and result TTL expired)
52    //
53    // In the WAL we can find multiple times the same procedure as UPDATE or INSERT.
54    // We read the WAL from top to bottom, so every time we find an entry of the
55    // same procedure, that will be the "latest" update.
56    //
57    // We keep two in-memory maps:
58    //  - localProcedureMap: is the map containing the entries in the WAL we are processing
59    //  - procedureMap: is the map containing all the procedures we found up to the WAL in process.
60    // localProcedureMap is merged with the procedureMap once we reach the WAL EOF.
61    //
62    // Since we are reading the WALs in reverse order (newest to oldest),
63    // if we find an entry related to a procedure we already have in 'procedureMap' we can discard it.
64    //
65    // The WAL is append-only so the last procedure in the WAL is the one that
66    // was in execution at the time we crashed/closed the server.
67    // given that, the procedure replay order can be inferred by the WAL order.
68    //
69    // Example:
70    //    WAL-2: [A, B, A, C, D]
71    //    WAL-1: [F, G, A, F, B]
72    //    Replay-Order: [D, C, A, B, F, G]
73    //
74    // The "localProcedureMap" keeps a "replayOrder" list. Every time we add the
75    // record to the map that record is moved to the head of the "replayOrder" list.
76    // Using the example above:
77    //    WAL-2 localProcedureMap.replayOrder is [D, C, A, B]
78    //    WAL-1 localProcedureMap.replayOrder is [F, G]
79    //
80    // each time we reach the WAL-EOF, the "replayOrder" list is merged/appended in 'procedureMap'
81    // so using the example above we end up with: [D, C, A, B] + [F, G] as replay order.
82    //
83    //  Fast Start: INIT/INSERT record and StackIDs
84    // ---------------------------------------------
85    // We have two special record, INIT and INSERT that tracks the first time
86    // the procedure was added to the WAL. We can use that information to be able
87    // to start procedures before reaching the end of the WAL, or before reading all the WALs.
88    // but in some cases the WAL with that record can be already gone.
89    // In alternative we can use the stackIds on each procedure,
90    // to identify when a procedure is ready to start.
91    // If there are gaps in the sum of the stackIds we need to read more WALs.
92    //
93    // Example (all procs child of A):
94    //   WAL-2: [A, B]                   A stackIds = [0, 4], B stackIds = [1, 5]
95    //   WAL-1: [A, B, C, D]
96    //
97    // In the case above we need to read one more WAL to be able to consider
98    // the root procedure A and all children as ready.
99    // ==============================================================================================
100   private final WalProcedureMap localProcedureMap = new WalProcedureMap(1024);
101   private final WalProcedureMap procedureMap = new WalProcedureMap(1024);
102 
103   //private long compactionLogId;
104   private long maxProcId = 0;
105 
106   private final ProcedureStoreTracker tracker;
107   private final boolean hasFastStartSupport;
108 
109   public ProcedureWALFormatReader(final ProcedureStoreTracker tracker) {
110     this.tracker = tracker;
111     // we support fast-start only if we have a clean shutdown.
112     this.hasFastStartSupport = !tracker.isEmpty();
113   }
114 
115   public void read(ProcedureWALFile log, ProcedureWALFormat.Loader loader) throws IOException {
116     long count = 0;
117     FSDataInputStream stream = log.getStream();
118     try {
119       boolean hasMore = true;
120       while (hasMore) {
121         ProcedureWALEntry entry = ProcedureWALFormat.readEntry(stream);
122         if (entry == null) {
123           LOG.warn("nothing left to decode. exiting with missing EOF");
124           hasMore = false;
125           break;
126         }
127         count++;
128         switch (entry.getType()) {
129           case PROCEDURE_WAL_INIT:
130             readInitEntry(entry);
131             break;
132           case PROCEDURE_WAL_INSERT:
133             readInsertEntry(entry);
134             break;
135           case PROCEDURE_WAL_UPDATE:
136           case PROCEDURE_WAL_COMPACT:
137             readUpdateEntry(entry);
138             break;
139           case PROCEDURE_WAL_DELETE:
140             readDeleteEntry(entry);
141             break;
142           case PROCEDURE_WAL_EOF:
143             hasMore = false;
144             break;
145           default:
146             throw new CorruptedWALProcedureStoreException("Invalid entry: " + entry);
147         }
148       }
149       LOG.info("Read " + count + " entries in " + log);
150     } catch (InvalidProtocolBufferException e) {
151       LOG.error("While reading entry #" + count + " in " + log, e);
152       loader.markCorruptedWAL(log, e);
153     }
154 
155     if (!localProcedureMap.isEmpty()) {
156       log.setProcIds(localProcedureMap.getMinProcId(), localProcedureMap.getMaxProcId());
157       procedureMap.mergeTail(localProcedureMap);
158       //if (hasFastStartSupport) {
159         // TODO: Some procedure may be already runnables (see readInitEntry())
160         //       (we can also check the "update map" in the log trackers)
161         // --------------------------------------------------
162         //EntryIterator iter = procedureMap.fetchReady();
163         //if (iter != null) loader.load(iter);
164         // --------------------------------------------------
165       //}
166     }
167   }
168 
169   public void finalize(ProcedureWALFormat.Loader loader) throws IOException {
170     // notify the loader about the max proc ID
171     loader.setMaxProcId(maxProcId);
172 
173     // fetch the procedure ready to run.
174     ProcedureIterator procIter = procedureMap.fetchReady();
175     if (procIter != null) loader.load(procIter);
176 
177     // remaining procedures have missing link or dependencies
178     // consider them as corrupted, manual fix is probably required.
179     procIter = procedureMap.fetchAll();
180     if (procIter != null) loader.handleCorrupted(procIter);
181   }
182 
183   private void loadProcedure(final ProcedureWALEntry entry, final ProcedureProtos.Procedure proc) {
184     maxProcId = Math.max(maxProcId, proc.getProcId());
185     if (isRequired(proc.getProcId())) {
186       if (LOG.isTraceEnabled()) {
187         LOG.trace("read " + entry.getType() + " entry " + proc.getProcId());
188       }
189       localProcedureMap.add(proc);
190       tracker.setDeleted(proc.getProcId(), false);
191     }
192   }
193 
194   private void readInitEntry(final ProcedureWALEntry entry)
195       throws IOException {
196     assert entry.getProcedureCount() == 1 : "Expected only one procedure";
197     loadProcedure(entry, entry.getProcedure(0));
198   }
199 
200   private void readInsertEntry(final ProcedureWALEntry entry) throws IOException {
201     assert entry.getProcedureCount() >= 1 : "Expected one or more procedures";
202     loadProcedure(entry, entry.getProcedure(0));
203     for (int i = 1; i < entry.getProcedureCount(); ++i) {
204       loadProcedure(entry, entry.getProcedure(i));
205     }
206   }
207 
208   private void readUpdateEntry(final ProcedureWALEntry entry) throws IOException {
209     assert entry.getProcedureCount() == 1 : "Expected only one procedure";
210     loadProcedure(entry, entry.getProcedure(0));
211   }
212 
213   private void readDeleteEntry(final ProcedureWALEntry entry) throws IOException {
214     assert entry.hasProcId() : "expected ProcID";
215 
216     if (entry.getChildIdCount() > 0) {
217       assert entry.getProcedureCount() == 1 : "Expected only one procedure";
218 
219       // update the parent procedure
220       loadProcedure(entry, entry.getProcedure(0));
221 
222       // remove the child procedures of entry.getProcId()
223       for (int i = 0, count = entry.getChildIdCount(); i < count; ++i) {
224         deleteEntry(entry.getChildId(i));
225       }
226     } else {
227       assert entry.getProcedureCount() == 0 : "Expected no procedures";
228 
229       // delete the procedure
230       deleteEntry(entry.getProcId());
231     }
232   }
233 
234   private void deleteEntry(final long procId) {
235     if (LOG.isTraceEnabled()) {
236       LOG.trace("delete entry " + procId);
237     }
238     maxProcId = Math.max(maxProcId, procId);
239     localProcedureMap.remove(procId);
240     assert !procedureMap.contains(procId);
241     tracker.setDeleted(procId, true);
242   }
243 
244   private boolean isDeleted(final long procId) {
245     return tracker.isDeleted(procId) == ProcedureStoreTracker.DeleteState.YES;
246   }
247 
248   private boolean isRequired(final long procId) {
249     return !isDeleted(procId) && !procedureMap.contains(procId);
250   }
251 
252   // ==========================================================================
253   //  We keep an in-memory map of the procedures sorted by replay order.
254   //  (see the details in the beginning of the file)
255   //                      _______________________________________________
256   //      procedureMap = | A |   | E |   | C |   |   |   |   | G |   |   |
257   //                       D               B
258   //      replayOrderHead = C <-> B <-> E <-> D <-> A <-> G
259   //
260   //  We also have a lazy grouping by "root procedure", and a list of
261   //  unlinked procedure. If after reading all the WALs we have unlinked
262   //  procedures it means that we had a missing WAL or a corruption.
263   //      rootHead = A <-> D <-> G
264   //                 B     E
265   //                 C
266   //      unlinkFromLinkList = None
267   // ==========================================================================
268   private static class Entry {
269     // hash-table next
270     protected Entry hashNext;
271     // child head
272     protected Entry childHead;
273     // double-link for rootHead or childHead
274     protected Entry linkNext;
275     protected Entry linkPrev;
276     // replay double-linked-list
277     protected Entry replayNext;
278     protected Entry replayPrev;
279     // procedure-infos
280     protected Procedure procedure;
281     protected ProcedureProtos.Procedure proto;
282     protected boolean ready = false;
283 
284     public Entry(Entry hashNext) { this.hashNext = hashNext; }
285 
286     public long getProcId() { return proto.getProcId(); }
287     public long getParentId() { return proto.getParentId(); }
288     public boolean hasParent() { return proto.hasParentId(); }
289     public boolean isReady() { return ready; }
290 
291     public boolean isCompleted() {
292       if (!hasParent()) {
293         // we only consider 'root' procedures. because for the user 'completed'
294         // means when everything up to the 'root' is complete.
295         switch (proto.getState()) {
296           case ROLLEDBACK:
297             return true;
298           case FINISHED:
299             return !proto.hasException();
300           default:
301             break;
302         }
303       }
304       return false;
305     }
306 
307     public Procedure convert() throws IOException {
308       if (procedure == null) {
309         procedure = Procedure.convert(proto);
310       }
311       return procedure;
312     }
313 
314     public ProcedureInfo convertToInfo() {
315       return ProcedureInfo.convert(proto);
316     }
317 
318     @Override
319     public String toString() {
320       final StringBuilder sb = new StringBuilder();
321       sb.append("Entry(");
322       sb.append(getProcId());
323       sb.append(", parentId=");
324       sb.append(getParentId());
325       sb.append(", class=");
326       sb.append(proto.getClassName());
327       sb.append(")");
328       return sb.toString();
329     }
330   }
331 
332   private static class EntryIterator implements ProcedureIterator {
333     private final Entry replayHead;
334     private Entry current;
335 
336     public EntryIterator(Entry replayHead) {
337       this.replayHead = replayHead;
338       this.current = replayHead;
339     }
340 
341     @Override
342     public void reset() {
343       this.current = replayHead;
344     }
345 
346     @Override
347     public boolean hasNext() {
348       return current != null;
349     }
350 
351     @Override
352     public boolean isNextCompleted() {
353       return current != null && current.isCompleted();
354     }
355 
356     @Override
357     public void skipNext() {
358       current = current.replayNext;
359     }
360 
361     @Override
362     public Procedure nextAsProcedure() throws IOException {
363       try {
364         return current.convert();
365       } finally {
366         current = current.replayNext;
367       }
368     }
369 
370     @Override
371     public ProcedureInfo nextAsProcedureInfo() {
372       try {
373         return current.convertToInfo();
374       } finally {
375         current = current.replayNext;
376       }
377     }
378   }
379 
380   private static class WalProcedureMap {
381     // procedure hash table
382     private Entry[] procedureMap;
383 
384     // replay-order double-linked-list
385     private Entry replayOrderHead;
386     private Entry replayOrderTail;
387 
388     // root linked-list
389     private Entry rootHead;
390 
391     // pending unlinked children (root not present yet)
392     private Entry childUnlinkedHead;
393 
394     // Track ProcId range
395     private long minProcId = Long.MAX_VALUE;
396     private long maxProcId = Long.MIN_VALUE;
397 
398     public WalProcedureMap(int size) {
399       procedureMap = new Entry[size];
400       replayOrderHead = null;
401       replayOrderTail = null;
402       rootHead = null;
403       childUnlinkedHead = null;
404     }
405 
406     public void add(ProcedureProtos.Procedure procProto) {
407       trackProcIds(procProto.getProcId());
408       Entry entry = addToMap(procProto.getProcId(), procProto.hasParentId());
409       boolean isNew = entry.proto == null;
410       entry.proto = procProto;
411       addToReplayList(entry);
412 
413       if (isNew) {
414         if (procProto.hasParentId()) {
415           childUnlinkedHead = addToLinkList(entry, childUnlinkedHead);
416         } else {
417           rootHead = addToLinkList(entry, rootHead);
418         }
419       }
420     }
421 
422     public boolean remove(long procId) {
423       trackProcIds(procId);
424       Entry entry = removeFromMap(procId);
425       if (entry != null) {
426         unlinkFromReplayList(entry);
427         unlinkFromLinkList(entry);
428         return true;
429       }
430       return false;
431     }
432 
433     private void trackProcIds(long procId) {
434       minProcId = Math.min(minProcId, procId);
435       maxProcId = Math.max(maxProcId, procId);
436     }
437 
438     public long getMinProcId() {
439       return minProcId;
440     }
441 
442     public long getMaxProcId() {
443       return maxProcId;
444     }
445 
446     public boolean contains(long procId) {
447       return getProcedure(procId) != null;
448     }
449 
450     public boolean isEmpty() {
451       return replayOrderHead == null;
452     }
453 
454     public void clear() {
455       for (int i = 0; i < procedureMap.length; ++i) {
456         procedureMap[i] = null;
457       }
458       replayOrderHead = null;
459       replayOrderTail = null;
460       rootHead = null;
461       childUnlinkedHead = null;
462       minProcId = Long.MAX_VALUE;
463       maxProcId = Long.MIN_VALUE;
464     }
465 
466     /*
467      * Merges two WalProcedureMap,
468      * the target is the "global" map, the source is the "local" map.
469      *  - The entries in the hashtables are guaranteed to be unique.
470      *    On replay we don't load procedures that already exist in the "global"
471      *    map (the one we are merging the "local" in to).
472      *  - The replayOrderList of the "local" nao will be appended to the "global"
473      *    map replay list.
474      *  - The "local" map will be cleared at the end of the operation.
475      */
476     public void mergeTail(WalProcedureMap other) {
477       for (Entry p = other.replayOrderHead; p != null; p = p.replayNext) {
478         int slotIndex = getMapSlot(p.getProcId());
479         p.hashNext = procedureMap[slotIndex];
480         procedureMap[slotIndex] = p;
481       }
482 
483       if (replayOrderHead == null) {
484         replayOrderHead = other.replayOrderHead;
485         replayOrderTail = other.replayOrderTail;
486         rootHead = other.rootHead;
487         childUnlinkedHead = other.childUnlinkedHead;
488       } else {
489         // append replay list
490         assert replayOrderTail.replayNext == null;
491         assert other.replayOrderHead.replayPrev == null;
492         replayOrderTail.replayNext = other.replayOrderHead;
493         other.replayOrderHead.replayPrev = replayOrderTail;
494         replayOrderTail = other.replayOrderTail;
495 
496         // merge rootHead
497         if (rootHead == null) {
498           rootHead = other.rootHead;
499         } else if (other.rootHead != null) {
500           Entry otherTail = findLinkListTail(other.rootHead);
501           otherTail.linkNext = rootHead;
502           rootHead.linkPrev = otherTail;
503           rootHead = other.rootHead;
504         }
505 
506         // merge childUnlinkedHead
507         if (childUnlinkedHead == null) {
508           childUnlinkedHead = other.childUnlinkedHead;
509         } else if (other.childUnlinkedHead != null) {
510           Entry otherTail = findLinkListTail(other.childUnlinkedHead);
511           otherTail.linkNext = childUnlinkedHead;
512           childUnlinkedHead.linkPrev = otherTail;
513           childUnlinkedHead = other.childUnlinkedHead;
514         }
515       }
516 
517       other.clear();
518     }
519 
520     /*
521      * Returns an EntryIterator with the list of procedures ready
522      * to be added to the executor.
523      * A Procedure is ready if its children and parent are ready.
524      */
525     public EntryIterator fetchReady() {
526       buildGraph();
527 
528       Entry readyHead = null;
529       Entry readyTail = null;
530       Entry p = replayOrderHead;
531       while (p != null) {
532         Entry next = p.replayNext;
533         if (p.isReady()) {
534           unlinkFromReplayList(p);
535           if (readyTail != null) {
536             readyTail.replayNext = p;
537             p.replayPrev = readyTail;
538           } else {
539             p.replayPrev = null;
540             readyHead = p;
541           }
542           readyTail = p;
543           p.replayNext = null;
544         }
545         p = next;
546       }
547       // we need the hash-table lookups for parents, so this must be done
548       // out of the loop where we check isReadyToRun()
549       for (p = readyHead; p != null; p = p.replayNext) {
550         removeFromMap(p.getProcId());
551         unlinkFromLinkList(p);
552       }
553       return readyHead != null ? new EntryIterator(readyHead) : null;
554     }
555 
556     /*
557      * Drain this map and return all procedures in it.
558      */
559     public EntryIterator fetchAll() {
560       Entry head = replayOrderHead;
561       for (Entry p = head; p != null; p = p.replayNext) {
562         removeFromMap(p.getProcId());
563       }
564       for (int i = 0; i < procedureMap.length; ++i) {
565         assert procedureMap[i] == null : "map not empty i=" + i;
566       }
567       replayOrderHead = null;
568       replayOrderTail = null;
569       childUnlinkedHead = null;
570       rootHead = null;
571       return head != null ? new EntryIterator(head) : null;
572     }
573 
574     private void buildGraph() {
575       Entry p = childUnlinkedHead;
576       while (p != null) {
577         Entry next = p.linkNext;
578         Entry rootProc = getRootProcedure(p);
579         if (rootProc != null) {
580           rootProc.childHead = addToLinkList(p, rootProc.childHead);
581         }
582         p = next;
583       }
584 
585       for (p = rootHead; p != null; p = p.linkNext) {
586         checkReadyToRun(p);
587       }
588     }
589 
590     private Entry getRootProcedure(Entry entry) {
591       while (entry != null && entry.hasParent()) {
592         entry = getProcedure(entry.getParentId());
593       }
594       return entry;
595     }
596 
597     /*
598      * (see the comprehensive explaination in the beginning of the file)
599      * A Procedure is ready when parent and children are ready.
600      * "ready" means that we all the information that we need in-memory.
601      *
602      * Example-1:
603      * We have two WALs, we start reading fronm the newest (wal-2)
604      *    wal-2 | C B |
605      *    wal-1 | A B C |
606      *
607      * If C and B don't depend on A (A is not the parent), we can start them
608      * before reading wal-1. If B is the only one with parent A we can start C
609      * and read one more WAL before being able to start B.
610      *
611      * How do we know with the only information in B that we are not ready.
612      *  - easy case, the parent is missing from the global map
613      *  - more complex case we look at the Stack IDs
614      *
615      * The Stack-IDs are added to the procedure order as incremental index
616      * tracking how many times that procedure was executed, which is equivalent
617      * at the number of times we wrote the procedure to the WAL.
618      * In the example above:
619      *   wal-2: B has stackId = [1, 2]
620      *   wal-1: B has stackId = [1]
621      *   wal-1: A has stackId = [0]
622      *
623      * Since we know that the Stack-IDs are incremental for a Procedure,
624      * we notice that there is a gap in the stackIds of B, so something was
625      * executed before.
626      * To identify when a Procedure is ready we do the sum of the stackIds of
627      * the procedure and the parent. if the stackIdSum is equals to the
628      * sum of {1..maxStackId} then everything we need is avaiable.
629      *
630      * Example-2
631      *    wal-2 | A |              A stackIds = [0, 2]
632      *    wal-1 | A B |            B stackIds = [1]
633      *
634      * There is a gap between A stackIds so something was executed in between.
635      */
636     private boolean checkReadyToRun(Entry rootEntry) {
637       assert !rootEntry.hasParent() : "expected root procedure, got " + rootEntry;
638 
639       if (rootEntry.isCompleted()) {
640         // if the root procedure is completed, sub-procedures should be gone
641         if (rootEntry.childHead != null) {
642           LOG.error("unexpected active children for root-procedure: " + rootEntry);
643           for (Entry p = rootEntry.childHead; p != null; p = p.linkNext) {
644             LOG.error("unexpected active children: " + p);
645           }
646         }
647 
648         assert rootEntry.childHead == null : "unexpected children on root completion. " + rootEntry;
649         rootEntry.ready = true;
650         return true;
651       }
652 
653       int stackIdSum = 0;
654       int maxStackId = 0;
655       for (int i = 0; i < rootEntry.proto.getStackIdCount(); ++i) {
656         int stackId = 1 + rootEntry.proto.getStackId(i);
657         maxStackId  = Math.max(maxStackId, stackId);
658         stackIdSum += stackId;
659       }
660 
661       for (Entry p = rootEntry.childHead; p != null; p = p.linkNext) {
662         for (int i = 0; i < p.proto.getStackIdCount(); ++i) {
663           int stackId = 1 + p.proto.getStackId(i);
664           maxStackId  = Math.max(maxStackId, stackId);
665           stackIdSum += stackId;
666         }
667       }
668       final int cmpStackIdSum = (maxStackId * (maxStackId + 1) / 2);
669       if (cmpStackIdSum == stackIdSum) {
670         rootEntry.ready = true;
671         for (Entry p = rootEntry.childHead; p != null; p = p.linkNext) {
672           p.ready = true;
673         }
674         return true;
675       }
676       return false;
677     }
678 
679     private void unlinkFromReplayList(Entry entry) {
680       if (replayOrderHead == entry) {
681         replayOrderHead = entry.replayNext;
682       }
683       if (replayOrderTail == entry) {
684         replayOrderTail = entry.replayPrev;
685       }
686       if (entry.replayPrev != null) {
687         entry.replayPrev.replayNext = entry.replayNext;
688       }
689       if (entry.replayNext != null) {
690         entry.replayNext.replayPrev = entry.replayPrev;
691       }
692     }
693 
694     private void addToReplayList(final Entry entry) {
695       unlinkFromReplayList(entry);
696       entry.replayNext = replayOrderHead;
697       entry.replayPrev = null;
698       if (replayOrderHead != null) {
699         replayOrderHead.replayPrev = entry;
700       } else {
701         replayOrderTail = entry;
702       }
703       replayOrderHead = entry;
704     }
705 
706     private void unlinkFromLinkList(Entry entry) {
707       if (entry == rootHead) {
708         rootHead = entry.linkNext;
709       } else if (entry == childUnlinkedHead) {
710         childUnlinkedHead = entry.linkNext;
711       }
712       if (entry.linkPrev != null) {
713         entry.linkPrev.linkNext = entry.linkNext;
714       }
715       if (entry.linkNext != null) {
716         entry.linkNext.linkPrev = entry.linkPrev;
717       }
718     }
719 
720     private Entry addToLinkList(Entry entry, Entry linkHead) {
721       unlinkFromLinkList(entry);
722       entry.linkNext = linkHead;
723       entry.linkPrev = null;
724       if (linkHead != null) {
725         linkHead.linkPrev = entry;
726       }
727       return entry;
728     }
729 
730     private Entry findLinkListTail(Entry linkHead) {
731       Entry tail = linkHead;
732       while (tail.linkNext != null) {
733         tail = tail.linkNext;
734       }
735       return tail;
736     }
737 
738     private Entry addToMap(final long procId, final boolean hasParent) {
739       int slotIndex = getMapSlot(procId);
740       Entry entry = getProcedure(slotIndex, procId);
741       if (entry != null) return entry;
742 
743       entry = new Entry(procedureMap[slotIndex]);
744       procedureMap[slotIndex] = entry;
745       return entry;
746     }
747 
748     private Entry removeFromMap(final long procId) {
749       int slotIndex = getMapSlot(procId);
750       Entry prev = null;
751       Entry entry = procedureMap[slotIndex];
752       while (entry != null) {
753         if (procId == entry.getProcId()) {
754           if (prev != null) {
755             prev.hashNext = entry.hashNext;
756           } else {
757             procedureMap[slotIndex] = entry.hashNext;
758           }
759           entry.hashNext = null;
760           return entry;
761         }
762         prev = entry;
763         entry = entry.hashNext;
764       }
765       return null;
766     }
767 
768     private Entry getProcedure(final long procId) {
769       return getProcedure(getMapSlot(procId), procId);
770     }
771 
772     private Entry getProcedure(final int slotIndex, final long procId) {
773       Entry entry = procedureMap[slotIndex];
774       while (entry != null) {
775         if (procId == entry.getProcId()) {
776           return entry;
777         }
778         entry = entry.hashNext;
779       }
780       return null;
781     }
782 
783     private int getMapSlot(final long procId) {
784       return (int)(Procedure.getProcIdHashCode(procId) % procedureMap.length);
785     }
786   }
787 }