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;
20  
21  import com.google.common.base.Preconditions;
22  
23  import java.io.IOException;
24  import java.util.ArrayList;
25  import java.util.Arrays;
26  import java.util.Collections;
27  import java.util.HashSet;
28  import java.util.Iterator;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.concurrent.atomic.AtomicBoolean;
32  import java.util.concurrent.atomic.AtomicInteger;
33  import java.util.concurrent.atomic.AtomicLong;
34  import java.util.concurrent.locks.ReentrantLock;
35  import java.util.concurrent.ConcurrentHashMap;
36  import java.util.concurrent.CopyOnWriteArrayList;
37  import java.util.concurrent.TimeUnit;
38  
39  import org.apache.commons.logging.Log;
40  import org.apache.commons.logging.LogFactory;
41  import org.apache.hadoop.conf.Configuration;
42  import org.apache.hadoop.hbase.HConstants;
43  import org.apache.hadoop.hbase.ProcedureInfo;
44  import org.apache.hadoop.hbase.classification.InterfaceAudience;
45  import org.apache.hadoop.hbase.classification.InterfaceStability;
46  import org.apache.hadoop.hbase.exceptions.IllegalArgumentIOException;
47  import org.apache.hadoop.hbase.procedure2.store.ProcedureStore;
48  import org.apache.hadoop.hbase.procedure2.store.ProcedureStore.ProcedureIterator;
49  import org.apache.hadoop.hbase.procedure2.util.StringUtils;
50  import org.apache.hadoop.hbase.procedure2.util.TimeoutBlockingQueue;
51  import org.apache.hadoop.hbase.procedure2.util.TimeoutBlockingQueue.TimeoutRetriever;
52  import org.apache.hadoop.hbase.protobuf.generated.ErrorHandlingProtos;
53  import org.apache.hadoop.hbase.protobuf.generated.ProcedureProtos.ProcedureState;
54  import org.apache.hadoop.hbase.security.User;
55  import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
56  import org.apache.hadoop.hbase.util.ForeignExceptionUtil;
57  import org.apache.hadoop.hbase.util.NonceKey;
58  import org.apache.hadoop.hbase.util.Pair;
59  import org.apache.hadoop.hbase.util.Threads;
60  
61  /**
62   * Thread Pool that executes the submitted procedures.
63   * The executor has a ProcedureStore associated.
64   * Each operation is logged and on restart the pending procedures are resumed.
65   *
66   * Unless the Procedure code throws an error (e.g. invalid user input)
67   * the procedure will complete (at some point in time), On restart the pending
68   * procedures are resumed and the once failed will be rolledback.
69   *
70   * The user can add procedures to the executor via submitProcedure(proc)
71   * check for the finished state via isFinished(procId)
72   * and get the result via getResult(procId)
73   */
74  @InterfaceAudience.Private
75  @InterfaceStability.Evolving
76  public class ProcedureExecutor<TEnvironment> {
77    private static final Log LOG = LogFactory.getLog(ProcedureExecutor.class);
78  
79    Testing testing = null;
80    public static class Testing {
81      protected boolean killBeforeStoreUpdate = false;
82      protected boolean toggleKillBeforeStoreUpdate = false;
83  
84      protected boolean shouldKillBeforeStoreUpdate() {
85        final boolean kill = this.killBeforeStoreUpdate;
86        if (this.toggleKillBeforeStoreUpdate) {
87          this.killBeforeStoreUpdate = !kill;
88          LOG.warn("Toggle Kill before store update to: " + this.killBeforeStoreUpdate);
89        }
90        return kill;
91      }
92    }
93  
94    public interface ProcedureExecutorListener {
95      void procedureLoaded(long procId);
96      void procedureAdded(long procId);
97      void procedureFinished(long procId);
98    }
99  
100   /**
101    * Used by the TimeoutBlockingQueue to get the timeout interval of the procedure
102    */
103   private static class ProcedureTimeoutRetriever implements TimeoutRetriever<Procedure> {
104     @Override
105     public long getTimeout(Procedure proc) {
106       return proc.getTimeRemaining();
107     }
108 
109     @Override
110     public TimeUnit getTimeUnit(Procedure proc) {
111       return TimeUnit.MILLISECONDS;
112     }
113   }
114 
115   /**
116    * Internal cleaner that removes the completed procedure results after a TTL.
117    * NOTE: This is a special case handled in timeoutLoop().
118    *
119    * Since the client code looks more or less like:
120    *   procId = master.doOperation()
121    *   while (master.getProcResult(procId) == ProcInProgress);
122    * The master should not throw away the proc result as soon as the procedure is done
123    * but should wait a result request from the client (see executor.removeResult(procId))
124    * The client will call something like master.isProcDone() or master.getProcResult()
125    * which will return the result/state to the client, and it will mark the completed
126    * proc as ready to delete. note that the client may not receive the response from
127    * the master (e.g. master failover) so, if we delay a bit the real deletion of
128    * the proc result the client will be able to get the result the next try.
129    */
130   private static class CompletedProcedureCleaner<TEnvironment>
131       extends ProcedureInMemoryChore<TEnvironment> {
132     private static final Log LOG = LogFactory.getLog(CompletedProcedureCleaner.class);
133 
134     private static final String CLEANER_INTERVAL_CONF_KEY = "hbase.procedure.cleaner.interval";
135     private static final int DEFAULT_CLEANER_INTERVAL = 30 * 1000; // 30sec
136 
137     private static final String EVICT_TTL_CONF_KEY = "hbase.procedure.cleaner.evict.ttl";
138     private static final int DEFAULT_EVICT_TTL = 15 * 60000; // 15min
139 
140     private static final String EVICT_ACKED_TTL_CONF_KEY ="hbase.procedure.cleaner.acked.evict.ttl";
141     private static final int DEFAULT_ACKED_EVICT_TTL = 5 * 60000; // 5min
142 
143     private final Map<Long, ProcedureInfo> completed;
144     private final Map<NonceKey, Long> nonceKeysToProcIdsMap;
145     private final ProcedureStore store;
146     private final Configuration conf;
147 
148     public CompletedProcedureCleaner(final Configuration conf, final ProcedureStore store,
149         final Map<Long, ProcedureInfo> completedMap,
150         final Map<NonceKey, Long> nonceKeysToProcIdsMap) {
151       // set the timeout interval that triggers the periodic-procedure
152       super(conf.getInt(CLEANER_INTERVAL_CONF_KEY, DEFAULT_CLEANER_INTERVAL));
153       this.completed = completedMap;
154       this.nonceKeysToProcIdsMap = nonceKeysToProcIdsMap;
155       this.store = store;
156       this.conf = conf;
157     }
158 
159     @Override
160     protected void periodicExecute(final TEnvironment env) {
161       if (completed.isEmpty()) {
162         if (LOG.isTraceEnabled()) {
163           LOG.trace("No completed procedures to cleanup.");
164         }
165         return;
166       }
167 
168       final long evictTtl = conf.getInt(EVICT_TTL_CONF_KEY, DEFAULT_EVICT_TTL);
169       final long evictAckTtl = conf.getInt(EVICT_ACKED_TTL_CONF_KEY, DEFAULT_ACKED_EVICT_TTL);
170 
171       final long now = EnvironmentEdgeManager.currentTime();
172       final Iterator<Map.Entry<Long, ProcedureInfo>> it = completed.entrySet().iterator();
173       final boolean isDebugEnabled = LOG.isDebugEnabled();
174       while (it.hasNext() && store.isRunning()) {
175         final Map.Entry<Long, ProcedureInfo> entry = it.next();
176         final ProcedureInfo procInfo = entry.getValue();
177 
178         // TODO: Select TTL based on Procedure type
179         if ((procInfo.hasClientAckTime() && (now - procInfo.getClientAckTime()) >= evictAckTtl) ||
180             (now - procInfo.getLastUpdate()) >= evictTtl) {
181           // Failed Procedures aren't persisted in WAL.
182           if (!(procInfo instanceof FailedProcedureInfo)) {
183             store.delete(entry.getKey());
184           }
185           it.remove();
186 
187           NonceKey nonceKey = procInfo.getNonceKey();
188           if (nonceKey != null) {
189             nonceKeysToProcIdsMap.remove(nonceKey);
190           }
191           if (isDebugEnabled) {
192             LOG.debug("Evict completed procedure: " + procInfo);
193           }
194         }
195       }
196     }
197   }
198 
199   /**
200    * Map the the procId returned by submitProcedure(), the Root-ProcID, to the ProcedureInfo.
201    * Once a Root-Procedure completes (success or failure), the result will be added to this map.
202    * The user of ProcedureExecutor should call getResult(procId) to get the result.
203    */
204   private final ConcurrentHashMap<Long, ProcedureInfo> completed =
205     new ConcurrentHashMap<Long, ProcedureInfo>();
206 
207   /**
208    * Map the the procId returned by submitProcedure(), the Root-ProcID, to the RootProcedureState.
209    * The RootProcedureState contains the execution stack of the Root-Procedure,
210    * It is added to the map by submitProcedure() and removed on procedure completion.
211    */
212   private final ConcurrentHashMap<Long, RootProcedureState> rollbackStack =
213     new ConcurrentHashMap<Long, RootProcedureState>();
214 
215   /**
216    * Helper map to lookup the live procedures by ID.
217    * This map contains every procedure. root-procedures and subprocedures.
218    */
219   private final ConcurrentHashMap<Long, Procedure> procedures =
220     new ConcurrentHashMap<Long, Procedure>();
221 
222   /**
223    * Helper map to lookup whether the procedure already issued from the same client.
224    * This map contains every root procedure.
225    */
226   private ConcurrentHashMap<NonceKey, Long> nonceKeysToProcIdsMap =
227       new ConcurrentHashMap<NonceKey, Long>();
228 
229   /**
230    * Timeout Queue that contains Procedures in a WAITING_TIMEOUT state
231    * or periodic procedures.
232    */
233   private final TimeoutBlockingQueue<Procedure> waitingTimeout =
234     new TimeoutBlockingQueue<Procedure>(new ProcedureTimeoutRetriever());
235 
236   /**
237    * Queue that contains runnable procedures.
238    */
239   private final ProcedureRunnableSet runnables;
240 
241   // TODO
242   private final ReentrantLock submitLock = new ReentrantLock();
243   private final AtomicLong lastProcId = new AtomicLong(-1);
244 
245   private final CopyOnWriteArrayList<ProcedureExecutorListener> listeners =
246     new CopyOnWriteArrayList<ProcedureExecutorListener>();
247 
248   private final AtomicInteger activeExecutorCount = new AtomicInteger(0);
249   private final AtomicBoolean running = new AtomicBoolean(false);
250   private final TEnvironment environment;
251   private final ProcedureStore store;
252   private final Configuration conf;
253 
254   private Thread[] threads;
255 
256   public ProcedureExecutor(final Configuration conf, final TEnvironment environment,
257       final ProcedureStore store) {
258     this(conf, environment, store, new ProcedureSimpleRunQueue());
259   }
260 
261   public ProcedureExecutor(final Configuration conf, final TEnvironment environment,
262       final ProcedureStore store, final ProcedureRunnableSet runqueue) {
263     this.environment = environment;
264     this.runnables = runqueue;
265     this.store = store;
266     this.conf = conf;
267   }
268 
269   private void load(final boolean abortOnCorruption) throws IOException {
270     Preconditions.checkArgument(completed.isEmpty());
271     Preconditions.checkArgument(rollbackStack.isEmpty());
272     Preconditions.checkArgument(procedures.isEmpty());
273     Preconditions.checkArgument(waitingTimeout.isEmpty());
274     Preconditions.checkArgument(runnables.size() == 0);
275 
276     store.load(new ProcedureStore.ProcedureLoader() {
277       @Override
278       public void setMaxProcId(long maxProcId) {
279         assert lastProcId.get() < 0 : "expected only one call to setMaxProcId()";
280         LOG.debug("load procedures maxProcId=" + maxProcId);
281         lastProcId.set(maxProcId);
282       }
283 
284       @Override
285       public void load(ProcedureIterator procIter) throws IOException {
286         loadProcedures(procIter, abortOnCorruption);
287       }
288 
289       @Override
290       public void handleCorrupted(ProcedureIterator procIter) throws IOException {
291         int corruptedCount = 0;
292         while (procIter.hasNext()) {
293           ProcedureInfo proc = procIter.nextAsProcedureInfo();
294           LOG.error("corrupted procedure: " + proc);
295           corruptedCount++;
296         }
297         if (abortOnCorruption && corruptedCount > 0) {
298           throw new IOException("found " + corruptedCount + " corrupted procedure(s) on replay");
299         }
300       }
301     });
302   }
303 
304   private void loadProcedures(final ProcedureIterator procIter,
305       final boolean abortOnCorruption) throws IOException {
306     final boolean isDebugEnabled = LOG.isDebugEnabled();
307 
308     // 1. Build the rollback stack
309     int runnablesCount = 0;
310     while (procIter.hasNext()) {
311       final NonceKey nonceKey;
312       final long procId;
313 
314       if (procIter.isNextCompleted()) {
315         ProcedureInfo proc = procIter.nextAsProcedureInfo();
316         nonceKey = proc.getNonceKey();
317         procId = proc.getProcId();
318         completed.put(proc.getProcId(), proc);
319         if (isDebugEnabled) {
320           LOG.debug("The procedure is completed: " + proc);
321         }
322       } else {
323         Procedure proc = procIter.nextAsProcedure();
324         nonceKey = proc.getNonceKey();
325         procId = proc.getProcId();
326 
327         if (!proc.hasParent()) {
328           assert !proc.isFinished() : "unexpected finished procedure";
329           rollbackStack.put(proc.getProcId(), new RootProcedureState());
330         }
331 
332         // add the procedure to the map
333         proc.beforeReplay(getEnvironment());
334         procedures.put(proc.getProcId(), proc);
335 
336         if (proc.getState() == ProcedureState.RUNNABLE) {
337           runnablesCount++;
338         }
339       }
340 
341       // add the nonce to the map
342       if (nonceKey != null) {
343         nonceKeysToProcIdsMap.put(nonceKey, procId);
344       }
345     }
346 
347     // 2. Initialize the stacks
348     ArrayList<Procedure> runnableList = new ArrayList(runnablesCount);
349     HashSet<Procedure> waitingSet = null;
350     procIter.reset();
351     while (procIter.hasNext()) {
352       if (procIter.isNextCompleted()) {
353         procIter.skipNext();
354         continue;
355       }
356 
357       Procedure proc = procIter.nextAsProcedure();
358       assert !(proc.isFinished() && !proc.hasParent()) : "unexpected completed proc=" + proc;
359 
360       if (isDebugEnabled) {
361         LOG.debug(String.format("Loading procedure state=%s isFailed=%s: %s",
362                     proc.getState(), proc.hasException(), proc));
363       }
364 
365       Long rootProcId = getRootProcedureId(proc);
366       if (rootProcId == null) {
367         // The 'proc' was ready to run but the root procedure was rolledback?
368         runnables.addBack(proc);
369         continue;
370       }
371 
372       if (proc.hasParent()) {
373         Procedure parent = procedures.get(proc.getParentProcId());
374         // corrupted procedures are handled later at step 3
375         if (parent != null && !proc.isFinished()) {
376           parent.incChildrenLatch();
377         }
378       }
379 
380       RootProcedureState procStack = rollbackStack.get(rootProcId);
381       procStack.loadStack(proc);
382 
383       switch (proc.getState()) {
384         case RUNNABLE:
385           runnableList.add(proc);
386           break;
387         case WAITING:
388           if (!proc.hasChildren()) {
389             runnableList.add(proc);
390           }
391           break;
392         case WAITING_TIMEOUT:
393           if (waitingSet == null) {
394             waitingSet = new HashSet<Procedure>();
395           }
396           waitingSet.add(proc);
397           break;
398         case FINISHED:
399           if (proc.hasException()) {
400             // add the proc to the runnables to perform the rollback
401             runnables.addBack(proc);
402           }
403           break;
404         case ROLLEDBACK:
405         case INITIALIZING:
406           String msg = "Unexpected " + proc.getState() + " state for " + proc;
407           LOG.error(msg);
408           throw new UnsupportedOperationException(msg);
409         default:
410           break;
411       }
412     }
413 
414     // 3. Validate the stacks
415     int corruptedCount = 0;
416     Iterator<Map.Entry<Long, RootProcedureState>> itStack = rollbackStack.entrySet().iterator();
417     while (itStack.hasNext()) {
418       Map.Entry<Long, RootProcedureState> entry = itStack.next();
419       RootProcedureState procStack = entry.getValue();
420       if (procStack.isValid()) continue;
421 
422       for (Procedure proc: procStack.getSubproceduresStack()) {
423         LOG.error("corrupted procedure: " + proc);
424         procedures.remove(proc.getProcId());
425         runnableList.remove(proc);
426         if (waitingSet != null) waitingSet.remove(proc);
427         corruptedCount++;
428       }
429       itStack.remove();
430     }
431 
432     if (abortOnCorruption && corruptedCount > 0) {
433       throw new IOException("found " + corruptedCount + " procedures on replay");
434     }
435 
436     // 4. Push the runnables
437     if (!runnableList.isEmpty()) {
438       // TODO: See ProcedureWALFormatReader#hasFastStartSupport
439       // some procedure may be started way before this stuff.
440       for (int i = runnableList.size() - 1; i >= 0; --i) {
441         Procedure proc = runnableList.get(i);
442         if (!proc.hasParent()) {
443           sendProcedureLoadedNotification(proc.getProcId());
444         }
445         if (proc.wasExecuted()) {
446           runnables.addFront(proc);
447         } else {
448           // if it was not in execution, it can wait.
449           runnables.addBack(proc);
450         }
451       }
452     }
453   }
454 
455   /**
456    * Start the procedure executor.
457    * It calls ProcedureStore.recoverLease() and ProcedureStore.load() to
458    * recover the lease, and ensure a single executor, and start the procedure
459    * replay to resume and recover the previous pending and in-progress perocedures.
460    *
461    * @param numThreads number of threads available for procedure execution.
462    * @param abortOnCorruption true if you want to abort your service in case
463    *          a corrupted procedure is found on replay. otherwise false.
464    */
465   public void start(int numThreads, boolean abortOnCorruption) throws IOException {
466     if (running.getAndSet(true)) {
467       LOG.warn("Already running");
468       return;
469     }
470 
471     // We have numThreads executor + one timer thread used for timing out
472     // procedures and triggering periodic procedures.
473     threads = new Thread[numThreads + 1];
474     LOG.info("Starting procedure executor threads=" + threads.length);
475 
476     // Initialize procedures executor
477     for (int i = 0; i < numThreads; ++i) {
478       threads[i] = new Thread("ProcedureExecutor-" + i) {
479         @Override
480         public void run() {
481           execLoop();
482         }
483       };
484     }
485 
486     // Initialize procedures timeout handler (this is the +1 thread)
487     threads[numThreads] = new Thread("ProcedureExecutorTimeout") {
488       @Override
489       public void run() {
490         timeoutLoop();
491       }
492     };
493 
494     // Acquire the store lease.
495     store.recoverLease();
496 
497     // TODO: Split in two steps.
498     // TODO: Handle corrupted procedures (currently just a warn)
499     // The first one will make sure that we have the latest id,
500     // so we can start the threads and accept new procedures.
501     // The second step will do the actual load of old procedures.
502     load(abortOnCorruption);
503 
504     // Start the executors. Here we must have the lastProcId set.
505     for (int i = 0; i < threads.length; ++i) {
506       threads[i].start();
507     }
508 
509     // Add completed cleaner chore
510     addChore(new CompletedProcedureCleaner(conf, store, completed, nonceKeysToProcIdsMap));
511   }
512 
513   public void stop() {
514     if (!running.getAndSet(false)) {
515       return;
516     }
517 
518     LOG.info("Stopping the procedure executor");
519     runnables.signalAll();
520     waitingTimeout.signalAll();
521   }
522 
523   public void join() {
524     boolean interrupted = false;
525 
526     for (int i = 0; i < threads.length; ++i) {
527       try {
528         threads[i].join();
529       } catch (InterruptedException ex) {
530         interrupted = true;
531       }
532     }
533 
534     if (interrupted) {
535       Thread.currentThread().interrupt();
536     }
537 
538     completed.clear();
539     rollbackStack.clear();
540     procedures.clear();
541     nonceKeysToProcIdsMap.clear();
542     waitingTimeout.clear();
543     runnables.clear();
544     lastProcId.set(-1);
545   }
546 
547   public boolean isRunning() {
548     return running.get();
549   }
550 
551   /**
552    * @return the number of execution threads.
553    */
554   public int getNumThreads() {
555     return threads == null ? 0 : (threads.length - 1);
556   }
557 
558   public int getActiveExecutorCount() {
559     return activeExecutorCount.get();
560   }
561 
562   public TEnvironment getEnvironment() {
563     return this.environment;
564   }
565 
566   public ProcedureStore getStore() {
567     return this.store;
568   }
569 
570   public void registerListener(ProcedureExecutorListener listener) {
571     this.listeners.add(listener);
572   }
573 
574   public boolean unregisterListener(ProcedureExecutorListener listener) {
575     return this.listeners.remove(listener);
576   }
577 
578   /**
579    * List procedures.
580    * @return the procedures in a list
581    */
582   public List<ProcedureInfo> listProcedures() {
583     List<ProcedureInfo> procedureLists =
584         new ArrayList<ProcedureInfo>(procedures.size() + completed.size());
585     for (java.util.Map.Entry<Long, Procedure> p: procedures.entrySet()) {
586       procedureLists.add(Procedure.createProcedureInfo(p.getValue(), null));
587     }
588     for (java.util.Map.Entry<Long, ProcedureInfo> e: completed.entrySet()) {
589       // Note: The procedure could show up twice in the list with different state, as
590       // it could complete after we walk through procedures list and insert into
591       // procedureList - it is ok, as we will use the information in the ProcedureInfo
592       // to figure it out; to prevent this would increase the complexity of the logic.
593       procedureLists.add(e.getValue());
594     }
595     return procedureLists;
596   }
597 
598   /**
599    * Add a chore procedure to the executor
600    * @param chore the chore to add
601    */
602   public void addChore(final ProcedureInMemoryChore chore) {
603     chore.setState(ProcedureState.RUNNABLE);
604     waitingTimeout.add(chore);
605   }
606 
607   /**
608    * Remove a chore procedure from the executor
609    * @param chore the chore to remove
610    * @return whether the chore is removed, or it will be removed later
611    */
612   public boolean removeChore(final ProcedureInMemoryChore chore) {
613     chore.setState(ProcedureState.FINISHED);
614     return waitingTimeout.remove(chore);
615   }
616 
617   // ==========================================================================
618   //  Nonce Procedure helpers
619   // ==========================================================================
620   /**
621    * Create a NoneKey from the specified nonceGroup and nonce.
622    * @param nonceGroup
623    * @param nonce
624    * @return the generated NonceKey
625    */
626   public NonceKey createNonceKey(final long nonceGroup, final long nonce) {
627     return (nonce == HConstants.NO_NONCE) ? null : new NonceKey(nonceGroup, nonce);
628   }
629 
630   /**
631    * Register a nonce for a procedure that is going to be submitted.
632    * A procId will be reserved and on submitProcedure(),
633    * the procedure with the specified nonce will take the reserved ProcId.
634    * If someone already reserved the nonce, this method will return the procId reserved,
635    * otherwise an invalid procId will be returned. and the caller should procede
636    * and submit the procedure.
637    *
638    * @param nonceKey A unique identifier for this operation from the client or process.
639    * @return the procId associated with the nonce, if any otherwise an invalid procId.
640    */
641   public long registerNonce(final NonceKey nonceKey) {
642     if (nonceKey == null) return -1;
643 
644     // check if we have already a Reserved ID for the nonce
645     Long oldProcId = nonceKeysToProcIdsMap.get(nonceKey);
646     if (oldProcId == null) {
647       // reserve a new Procedure ID, this will be associated with the nonce
648       // and the procedure submitted with the specified nonce will use this ID.
649       final long newProcId = nextProcId();
650       oldProcId = nonceKeysToProcIdsMap.putIfAbsent(nonceKey, newProcId);
651       if (oldProcId == null) return -1;
652     }
653 
654     // we found a registered nonce, but the procedure may not have been submitted yet.
655     // since the client expect the procedure to be submitted, spin here until it is.
656     final boolean isTraceEnabled = LOG.isTraceEnabled();
657     while (isRunning() &&
658            !(procedures.containsKey(oldProcId) || completed.containsKey(oldProcId)) &&
659            nonceKeysToProcIdsMap.containsKey(nonceKey)) {
660       if (isTraceEnabled) {
661         LOG.trace("waiting for procId=" + oldProcId.longValue() + " to be submitted");
662       }
663       Threads.sleep(100);
664     }
665     return oldProcId.longValue();
666   }
667 
668   /**
669    * Remove the NonceKey if the procedure was not submitted to the executor.
670    * @param nonceKey A unique identifier for this operation from the client or process.
671    */
672   public void unregisterNonceIfProcedureWasNotSubmitted(final NonceKey nonceKey) {
673     if (nonceKey == null) return;
674 
675     final Long procId = nonceKeysToProcIdsMap.get(nonceKey);
676     if (procId == null) return;
677 
678     // if the procedure was not submitted, remove the nonce
679     if (!(procedures.containsKey(procId) || completed.containsKey(procId))) {
680       nonceKeysToProcIdsMap.remove(nonceKey);
681     }
682   }
683 
684   /**
685    * If the failure failed before submitting it, we may want to give back the
686    * same error to the requests with the same nonceKey.
687    *
688    * @param nonceKey A unique identifier for this operation from the client or process
689    * @param procName name of the procedure, used to inform the user
690    * @param procOwner name of the owner of the procedure, used to inform the user
691    * @param exception the failure to report to the user
692    */
693   public void setFailureResultForNonce(final NonceKey nonceKey, final String procName,
694       final User procOwner, final IOException exception) {
695     if (nonceKey == null) return;
696 
697     final Long procId = nonceKeysToProcIdsMap.get(nonceKey);
698     if (procId == null || completed.containsKey(procId)) return;
699 
700     final long currentTime = EnvironmentEdgeManager.currentTime();
701     final ProcedureInfo result = new FailedProcedureInfo(
702       procId.longValue(),
703       procName,
704       procOwner != null ? procOwner.getShortName() : null,
705       ProcedureState.ROLLEDBACK,
706       -1,
707       nonceKey,
708       ForeignExceptionUtil.toProtoForeignException("ProcedureExecutor", exception),
709       currentTime,
710       currentTime,
711       null);
712     completed.putIfAbsent(procId, result);
713   }
714 
715   public static class FailedProcedureInfo extends ProcedureInfo {
716 
717     public FailedProcedureInfo(long procId, String procName, String procOwner,
718         ProcedureState procState, long parentId, NonceKey nonceKey,
719         ErrorHandlingProtos.ForeignExceptionMessage exception, long lastUpdate, long startTime,
720         byte[] result) {
721       super(procId, procName, procOwner, procState, parentId, nonceKey, exception, lastUpdate,
722           startTime, result);
723     }
724   }
725 
726   // ==========================================================================
727   //  Submit/Abort Procedure
728   // ==========================================================================
729   /**
730    * Add a new root-procedure to the executor.
731    * @param proc the new procedure to execute.
732    * @return the procedure id, that can be used to monitor the operation
733    */
734   public long submitProcedure(final Procedure proc) {
735     return submitProcedure(proc, null);
736   }
737 
738   /**
739    * Add a new root-procedure to the executor.
740    * @param proc the new procedure to execute.
741    * @param nonceKey the registered unique identifier for this operation from the client or process.
742    * @return the procedure id, that can be used to monitor the operation
743    */
744   @edu.umd.cs.findbugs.annotations.SuppressWarnings(value="NP_NULL_ON_SOME_PATH",
745       justification = "FindBugs is blind to the check-for-null")
746   public long submitProcedure(final Procedure proc, final NonceKey nonceKey) {
747     Preconditions.checkArgument(proc.getState() == ProcedureState.INITIALIZING);
748     Preconditions.checkArgument(isRunning(), "executor not running");
749     Preconditions.checkArgument(lastProcId.get() >= 0);
750     Preconditions.checkArgument(!proc.hasParent(), "unexpected parent", proc);
751 
752     final Long currentProcId;
753     if (nonceKey != null) {
754       currentProcId = nonceKeysToProcIdsMap.get(nonceKey);
755       Preconditions.checkArgument(currentProcId != null,
756         "expected nonceKey=" + nonceKey + " to be reserved, use registerNonce()");
757     } else {
758       currentProcId = nextProcId();
759     }
760 
761     // Initialize the procedure
762     proc.setNonceKey(nonceKey);
763     proc.setProcId(currentProcId.longValue());
764 
765     // Commit the transaction
766     store.insert(proc, null);
767     if (LOG.isDebugEnabled()) {
768       LOG.debug("Procedure " + proc + " added to the store.");
769     }
770 
771     // Create the rollback stack for the procedure
772     RootProcedureState stack = new RootProcedureState();
773     rollbackStack.put(currentProcId, stack);
774 
775     // Submit the new subprocedures
776     assert !procedures.containsKey(currentProcId);
777     procedures.put(currentProcId, proc);
778     sendProcedureAddedNotification(currentProcId);
779     runnables.addBack(proc);
780     return currentProcId;
781   }
782 
783   public ProcedureInfo getResult(final long procId) {
784     return completed.get(procId);
785   }
786 
787   /**
788    * Return true if the procedure is finished.
789    * The state may be "completed successfully" or "failed and rolledback".
790    * Use getResult() to check the state or get the result data.
791    * @param procId the ID of the procedure to check
792    * @return true if the procedure execution is finished, otherwise false.
793    */
794   public boolean isFinished(final long procId) {
795     return completed.containsKey(procId);
796   }
797 
798   /**
799    * Return true if the procedure is started.
800    * @param procId the ID of the procedure to check
801    * @return true if the procedure execution is started, otherwise false.
802    */
803   public boolean isStarted(final long procId) {
804     Procedure proc = procedures.get(procId);
805     if (proc == null) {
806       return completed.get(procId) != null;
807     }
808     return proc.wasExecuted();
809   }
810 
811   /**
812    * Mark the specified completed procedure, as ready to remove.
813    * @param procId the ID of the procedure to remove
814    */
815   public void removeResult(final long procId) {
816     ProcedureInfo result = completed.get(procId);
817     if (result == null) {
818       assert !procedures.containsKey(procId) : "procId=" + procId + " is still running";
819       if (LOG.isDebugEnabled()) {
820         LOG.debug("Procedure procId=" + procId + " already removed by the cleaner.");
821       }
822       return;
823     }
824 
825     // The CompletedProcedureCleaner will take care of deletion, once the TTL is expired.
826     result.setClientAckTime(EnvironmentEdgeManager.currentTime());
827   }
828 
829   /**
830    * Send an abort notification the specified procedure.
831    * Depending on the procedure implementation the abort can be considered or ignored.
832    * @param procId the procedure to abort
833    * @return true if the procedure exist and has received the abort, otherwise false.
834    */
835   public boolean abort(final long procId) {
836     return abort(procId, true);
837   }
838 
839   /**
840    * Send an abort notification the specified procedure.
841    * Depending on the procedure implementation the abort can be considered or ignored.
842    * @param procId the procedure to abort
843    * @param mayInterruptIfRunning if the proc completed at least one step, should it be aborted?
844    * @return true if the procedure exist and has received the abort, otherwise false.
845    */
846   public boolean abort(final long procId, final boolean mayInterruptIfRunning) {
847     Procedure proc = procedures.get(procId);
848     if (proc != null) {
849       if (!mayInterruptIfRunning && proc.wasExecuted()) {
850         return false;
851       } else {
852         return proc.abort(getEnvironment());
853       }
854     }
855     return false;
856   }
857 
858   /**
859    * Check if the user is this procedure's owner
860    * @param procId the target procedure
861    * @param user the user
862    * @return true if the user is the owner of the procedure,
863    *   false otherwise or the owner is unknown.
864    */
865   public boolean isProcedureOwner(final long procId, final User user) {
866     if (user == null) {
867       return false;
868     }
869 
870     Procedure proc = procedures.get(procId);
871     if (proc != null) {
872       return proc.getOwner().equals(user.getShortName());
873     }
874     ProcedureInfo procInfo = completed.get(procId);
875     if (procInfo == null) {
876       // Procedure either does not exist or has already completed and got cleaned up.
877       // At this time, we cannot check the owner of the procedure
878       return false;
879     }
880     return ProcedureInfo.isProcedureOwner(procInfo, user);
881   }
882 
883   public Map<Long, ProcedureInfo> getResults() {
884     return Collections.unmodifiableMap(completed);
885   }
886 
887   public Procedure getProcedure(final long procId) {
888     return procedures.get(procId);
889   }
890 
891   protected ProcedureRunnableSet getRunnableSet() {
892     return runnables;
893   }
894 
895   /**
896    * Execution loop (N threads)
897    * while the executor is in a running state,
898    * fetch a procedure from the runnables queue and start the execution.
899    */
900   private void execLoop() {
901     while (isRunning()) {
902       Procedure proc = runnables.poll();
903       if (proc == null) continue;
904 
905       try {
906         activeExecutorCount.incrementAndGet();
907         execLoop(proc);
908       } finally {
909         activeExecutorCount.decrementAndGet();
910       }
911     }
912   }
913 
914   private void execLoop(Procedure proc) {
915     if (LOG.isTraceEnabled()) {
916       LOG.trace("Trying to start the execution of " + proc);
917     }
918 
919     Long rootProcId = getRootProcedureId(proc);
920     if (rootProcId == null) {
921       // The 'proc' was ready to run but the root procedure was rolledback
922       executeRollback(proc);
923       return;
924     }
925 
926     RootProcedureState procStack = rollbackStack.get(rootProcId);
927     if (procStack == null) return;
928 
929     do {
930       // Try to acquire the execution
931       if (!procStack.acquire(proc)) {
932         if (procStack.setRollback()) {
933           // we have the 'rollback-lock' we can start rollingback
934           if (!executeRollback(rootProcId, procStack)) {
935             procStack.unsetRollback();
936             runnables.yield(proc);
937           }
938         } else {
939           // if we can't rollback means that some child is still running.
940           // the rollback will be executed after all the children are done.
941           // If the procedure was never executed, remove and mark it as rolledback.
942           if (!proc.wasExecuted()) {
943             if (!executeRollback(proc)) {
944               runnables.yield(proc);
945             }
946           }
947         }
948         break;
949       }
950 
951       // Execute the procedure
952       assert proc.getState() == ProcedureState.RUNNABLE;
953       if (proc.acquireLock(getEnvironment())) {
954         execProcedure(procStack, proc);
955         proc.releaseLock(getEnvironment());
956       } else {
957         runnables.yield(proc);
958       }
959       procStack.release(proc);
960 
961       // allows to kill the executor before something is stored to the wal.
962       // useful to test the procedure recovery.
963       if (testing != null && !isRunning()) {
964         break;
965       }
966 
967       if (proc.isSuccess()) {
968         if (LOG.isDebugEnabled()) {
969           LOG.debug("Procedure completed in " +
970               StringUtils.humanTimeDiff(proc.elapsedTime()) + ": " + proc);
971         }
972         // Finalize the procedure state
973         if (proc.getProcId() == rootProcId) {
974           procedureFinished(proc);
975         }
976         break;
977       }
978     } while (procStack.isFailed());
979   }
980 
981   private void timeoutLoop() {
982     while (isRunning()) {
983       Procedure proc = waitingTimeout.poll();
984       if (proc == null) continue;
985 
986       if (proc.getTimeRemaining() > 100) {
987         // got an early wake, maybe a stop?
988         // re-enqueue the task in case was not a stop or just a signal
989         waitingTimeout.add(proc);
990         continue;
991       }
992 
993       // ----------------------------------------------------------------------------
994       // TODO-MAYBE: Should we provide a notification to the store with the
995       // full set of procedures pending and completed to write a compacted
996       // version of the log (in case is a log)?
997       // In theory no, procedures are have a short life, so at some point the store
998       // will have the tracker saying everything is in the last log.
999       // ----------------------------------------------------------------------------
1000 
1001       // The ProcedureInMemoryChore is a special case, and it acts as a chore.
1002       // instead of bringing the Chore class in, we reuse this timeout thread for
1003       // this special case.
1004       if (proc instanceof ProcedureInMemoryChore) {
1005         if (proc.isRunnable()) {
1006           try {
1007             ((ProcedureInMemoryChore)proc).periodicExecute(getEnvironment());
1008           } catch (Throwable e) {
1009             LOG.error("Ignoring CompletedProcedureCleaner exception: " + e.getMessage(), e);
1010           }
1011           proc.setStartTime(EnvironmentEdgeManager.currentTime());
1012           if (proc.isRunnable()) waitingTimeout.add(proc);
1013         }
1014         continue;
1015       }
1016 
1017       // The procedure received an "abort-timeout", call abort() and
1018       // add the procedure back in the queue for rollback.
1019       if (proc.setTimeoutFailure()) {
1020         long rootProcId = Procedure.getRootProcedureId(procedures, proc);
1021         RootProcedureState procStack = rollbackStack.get(rootProcId);
1022         procStack.abort();
1023         store.update(proc);
1024         runnables.addFront(proc);
1025         continue;
1026       }
1027     }
1028   }
1029 
1030   /**
1031    * Execute the rollback of the full procedure stack.
1032    * Once the procedure is rolledback, the root-procedure will be visible as
1033    * finished to user, and the result will be the fatal exception.
1034    */
1035   private boolean executeRollback(final long rootProcId, final RootProcedureState procStack) {
1036     Procedure rootProc = procedures.get(rootProcId);
1037     RemoteProcedureException exception = rootProc.getException();
1038     if (exception == null) {
1039       exception = procStack.getException();
1040       rootProc.setFailure(exception);
1041       store.update(rootProc);
1042     }
1043 
1044     List<Procedure> subprocStack = procStack.getSubproceduresStack();
1045     assert subprocStack != null : "Called rollback with no steps executed rootProc=" + rootProc;
1046 
1047     int stackTail = subprocStack.size();
1048     boolean reuseLock = false;
1049     while (stackTail --> 0) {
1050       final Procedure proc = subprocStack.get(stackTail);
1051 
1052       if (!reuseLock && !proc.acquireLock(getEnvironment())) {
1053         // can't take a lock on the procedure, add the root-proc back on the
1054         // queue waiting for the lock availability
1055         return false;
1056       }
1057 
1058       boolean abortRollback = !executeRollback(proc);
1059       abortRollback |= !isRunning() || !store.isRunning();
1060 
1061       // If the next procedure is the same to this one
1062       // (e.g. StateMachineProcedure reuse the same instance)
1063       // we can avoid to lock/unlock each step
1064       reuseLock = stackTail > 0 && (subprocStack.get(stackTail - 1) == proc) && !abortRollback;
1065       if (!reuseLock) {
1066         proc.releaseLock(getEnvironment());
1067       }
1068 
1069       // allows to kill the executor before something is stored to the wal.
1070       // useful to test the procedure recovery.
1071       if (abortRollback) {
1072         return false;
1073       }
1074 
1075       subprocStack.remove(stackTail);
1076 
1077       // if the procedure is kind enough to pass the slot to someone else, yield
1078       if (proc.isYieldAfterExecutionStep(getEnvironment())) {
1079         return false;
1080       }
1081     }
1082 
1083     // Finalize the procedure state
1084     LOG.info("Rolledback procedure " + rootProc +
1085              " exec-time=" + StringUtils.humanTimeDiff(rootProc.elapsedTime()) +
1086              " exception=" + exception.getMessage());
1087     procedureFinished(rootProc);
1088     return true;
1089   }
1090 
1091   /**
1092    * Execute the rollback of the procedure step.
1093    * It updates the store with the new state (stack index)
1094    * or will remove completly the procedure in case it is a child.
1095    */
1096   private boolean executeRollback(final Procedure proc) {
1097     try {
1098       proc.doRollback(getEnvironment());
1099     } catch (IOException e) {
1100       if (LOG.isDebugEnabled()) {
1101         LOG.debug("rollback attempt failed for " + proc, e);
1102       }
1103       return false;
1104     } catch (InterruptedException e) {
1105       handleInterruptedException(proc, e);
1106       return false;
1107     } catch (Throwable e) {
1108       // Catch NullPointerExceptions or similar errors...
1109       LOG.fatal("CODE-BUG: Uncatched runtime exception for procedure: " + proc, e);
1110     }
1111 
1112     // allows to kill the executor before something is stored to the wal.
1113     // useful to test the procedure recovery.
1114     if (testing != null && testing.shouldKillBeforeStoreUpdate()) {
1115       LOG.debug("TESTING: Kill before store update");
1116       stop();
1117       return false;
1118     }
1119 
1120     if (proc.removeStackIndex()) {
1121       proc.setState(ProcedureState.ROLLEDBACK);
1122       if (proc.hasParent()) {
1123         store.delete(proc.getProcId());
1124         procedures.remove(proc.getProcId());
1125       } else {
1126         final long[] childProcIds = rollbackStack.get(proc.getProcId()).getSubprocedureIds();
1127         if (childProcIds != null) {
1128           store.delete(proc, childProcIds);
1129         } else {
1130           store.update(proc);
1131         }
1132       }
1133     } else {
1134       store.update(proc);
1135     }
1136 
1137     return true;
1138   }
1139 
1140   /**
1141    * Executes the specified procedure
1142    *  - calls the doExecute() of the procedure
1143    *  - if the procedure execution didn't fail (e.g. invalid user input)
1144    *     - ...and returned subprocedures
1145    *        - the subprocedures are initialized.
1146    *        - the subprocedures are added to the store
1147    *        - the subprocedures are added to the runnable queue
1148    *        - the procedure is now in a WAITING state, waiting for the subprocedures to complete
1149    *     - ...if there are no subprocedure
1150    *        - the procedure completed successfully
1151    *        - if there is a parent (WAITING)
1152    *            - the parent state will be set to RUNNABLE
1153    *  - in case of failure
1154    *    - the store is updated with the new state
1155    *    - the executor (caller of this method) will start the rollback of the procedure
1156    */
1157   private void execProcedure(final RootProcedureState procStack, final Procedure procedure) {
1158     Preconditions.checkArgument(procedure.getState() == ProcedureState.RUNNABLE);
1159 
1160     // Execute the procedure
1161     boolean reExecute = false;
1162     Procedure[] subprocs = null;
1163     do {
1164       reExecute = false;
1165       try {
1166         subprocs = procedure.doExecute(getEnvironment());
1167         if (subprocs != null && subprocs.length == 0) {
1168           subprocs = null;
1169         }
1170       } catch (ProcedureYieldException e) {
1171         if (LOG.isTraceEnabled()) {
1172           LOG.trace("Yield procedure: " + procedure + ": " + e.getMessage());
1173         }
1174         runnables.yield(procedure);
1175         return;
1176       } catch (InterruptedException e) {
1177         handleInterruptedException(procedure, e);
1178         runnables.yield(procedure);
1179         return;
1180       } catch (Throwable e) {
1181         // Catch NullPointerExceptions or similar errors...
1182         String msg = "CODE-BUG: Uncatched runtime exception for procedure: " + procedure;
1183         LOG.error(msg, e);
1184         procedure.setFailure(new RemoteProcedureException(msg, e));
1185       }
1186 
1187       if (!procedure.isFailed()) {
1188         if (subprocs != null) {
1189           if (subprocs.length == 1 && subprocs[0] == procedure) {
1190             // quick-shortcut for a state machine like procedure
1191             subprocs = null;
1192             reExecute = true;
1193           } else {
1194             // yield the current procedure, and make the subprocedure runnable
1195             for (int i = 0; i < subprocs.length; ++i) {
1196               Procedure subproc = subprocs[i];
1197               if (subproc == null) {
1198                 String msg = "subproc[" + i + "] is null, aborting the procedure";
1199                 procedure.setFailure(new RemoteProcedureException(msg,
1200                   new IllegalArgumentIOException(msg)));
1201                 subprocs = null;
1202                 break;
1203               }
1204 
1205               assert subproc.getState() == ProcedureState.INITIALIZING;
1206               subproc.setParentProcId(procedure.getProcId());
1207               subproc.setProcId(nextProcId());
1208               procStack.addSubProcedure(subproc);
1209             }
1210 
1211             if (!procedure.isFailed()) {
1212               procedure.setChildrenLatch(subprocs.length);
1213               switch (procedure.getState()) {
1214                 case RUNNABLE:
1215                   procedure.setState(ProcedureState.WAITING);
1216                   break;
1217                 case WAITING_TIMEOUT:
1218                   waitingTimeout.add(procedure);
1219                   break;
1220                 default:
1221                   break;
1222               }
1223             }
1224           }
1225         } else if (procedure.getState() == ProcedureState.WAITING_TIMEOUT) {
1226           waitingTimeout.add(procedure);
1227         } else {
1228           // No subtask, so we are done
1229           procedure.setState(ProcedureState.FINISHED);
1230         }
1231       }
1232 
1233       // Add the procedure to the stack
1234       procStack.addRollbackStep(procedure);
1235 
1236       // allows to kill the executor before something is stored to the wal.
1237       // useful to test the procedure recovery.
1238       if (testing != null && testing.shouldKillBeforeStoreUpdate()) {
1239         LOG.debug("TESTING: Kill before store update");
1240         stop();
1241         return;
1242       }
1243 
1244       // Commit the transaction
1245       updateStoreOnExec(procStack, procedure, subprocs);
1246 
1247       // if the store is not running we are aborting
1248       if (!store.isRunning()) {
1249         return;
1250       }
1251 
1252       // if the procedure is kind enough to pass the slot to someone else, yield
1253       if (procedure.getState() == ProcedureState.RUNNABLE &&
1254           procedure.isYieldAfterExecutionStep(getEnvironment())) {
1255         runnables.yield(procedure);
1256         return;
1257       }
1258 
1259       assert (reExecute && subprocs == null) || !reExecute;
1260     } while (reExecute);
1261 
1262     // Submit the new subprocedures
1263     if (subprocs != null && !procedure.isFailed()) {
1264       for (int i = 0; i < subprocs.length; ++i) {
1265         Procedure subproc = subprocs[i];
1266         assert !procedures.containsKey(subproc.getProcId());
1267         procedures.put(subproc.getProcId(), subproc);
1268         runnables.addFront(subproc);
1269       }
1270     }
1271 
1272     if (procedure.isFinished() && procedure.hasParent()) {
1273       Procedure parent = procedures.get(procedure.getParentProcId());
1274       if (parent == null) {
1275         assert procStack.isRollingback();
1276         return;
1277       }
1278 
1279       // If this procedure is the last child awake the parent procedure
1280       if (LOG.isTraceEnabled()) {
1281         LOG.trace(parent + " child is done: " + procedure);
1282       }
1283       if (parent.childrenCountDown() && parent.getState() == ProcedureState.WAITING) {
1284         parent.setState(ProcedureState.RUNNABLE);
1285         store.update(parent);
1286         runnables.addFront(parent);
1287         if (LOG.isTraceEnabled()) {
1288           LOG.trace(parent + " all the children finished their work, resume.");
1289         }
1290         return;
1291       }
1292     }
1293   }
1294 
1295   private void updateStoreOnExec(final RootProcedureState procStack,
1296       final Procedure procedure, final Procedure[] subprocs) {
1297     if (subprocs != null && !procedure.isFailed()) {
1298       if (LOG.isTraceEnabled()) {
1299         LOG.trace("Store add " + procedure + " children " + Arrays.toString(subprocs));
1300       }
1301       store.insert(procedure, subprocs);
1302     } else {
1303       if (LOG.isTraceEnabled()) {
1304         LOG.trace("Store update " + procedure);
1305       }
1306       if (procedure.isFinished() && !procedure.hasParent()) {
1307         // remove child procedures
1308         final long[] childProcIds = procStack.getSubprocedureIds();
1309         if (childProcIds != null) {
1310           store.delete(procedure, childProcIds);
1311           for (int i = 0; i < childProcIds.length; ++i) {
1312             procedures.remove(childProcIds[i]);
1313           }
1314         } else {
1315           store.update(procedure);
1316         }
1317       } else {
1318         store.update(procedure);
1319       }
1320     }
1321   }
1322 
1323   private void handleInterruptedException(final Procedure proc, final InterruptedException e) {
1324     if (LOG.isTraceEnabled()) {
1325       LOG.trace("got an interrupt during " + proc + ". suspend and retry it later.", e);
1326     }
1327 
1328     // NOTE: We don't call Thread.currentThread().interrupt()
1329     // because otherwise all the subsequent calls e.g. Thread.sleep() will throw
1330     // the InterruptedException. If the master is going down, we will be notified
1331     // and the executor/store will be stopped.
1332     // (The interrupted procedure will be retried on the next run)
1333   }
1334 
1335   private void sendProcedureLoadedNotification(final long procId) {
1336     if (!this.listeners.isEmpty()) {
1337       for (ProcedureExecutorListener listener: this.listeners) {
1338         try {
1339           listener.procedureLoaded(procId);
1340         } catch (Throwable e) {
1341           LOG.error("The listener " + listener + " had an error: " + e.getMessage(), e);
1342         }
1343       }
1344     }
1345   }
1346 
1347   private void sendProcedureAddedNotification(final long procId) {
1348     if (!this.listeners.isEmpty()) {
1349       for (ProcedureExecutorListener listener: this.listeners) {
1350         try {
1351           listener.procedureAdded(procId);
1352         } catch (Throwable e) {
1353           LOG.error("The listener " + listener + " had an error: " + e.getMessage(), e);
1354         }
1355       }
1356     }
1357   }
1358 
1359   private void sendProcedureFinishedNotification(final long procId) {
1360     if (!this.listeners.isEmpty()) {
1361       for (ProcedureExecutorListener listener: this.listeners) {
1362         try {
1363           listener.procedureFinished(procId);
1364         } catch (Throwable e) {
1365           LOG.error("The listener " + listener + " had an error: " + e.getMessage(), e);
1366         }
1367       }
1368     }
1369   }
1370 
1371   private long nextProcId() {
1372     long procId = lastProcId.incrementAndGet();
1373     if (procId < 0) {
1374       while (!lastProcId.compareAndSet(procId, 0)) {
1375         procId = lastProcId.get();
1376         if (procId >= 0)
1377           break;
1378       }
1379       while (procedures.containsKey(procId)) {
1380         procId = lastProcId.incrementAndGet();
1381       }
1382     }
1383     return procId;
1384   }
1385 
1386   protected long getLastProcId() {
1387     return lastProcId.get();
1388   }
1389 
1390   private Long getRootProcedureId(Procedure proc) {
1391     return Procedure.getRootProcedureId(procedures, proc);
1392   }
1393 
1394   private void procedureFinished(final Procedure proc) {
1395     // call the procedure completion cleanup handler
1396     try {
1397       proc.completionCleanup(getEnvironment());
1398     } catch (Throwable e) {
1399       // Catch NullPointerExceptions or similar errors...
1400       LOG.error("CODE-BUG: uncatched runtime exception for procedure: " + proc, e);
1401     }
1402 
1403     // update the executor internal state maps
1404     ProcedureInfo procInfo = Procedure.createProcedureInfo(proc, proc.getNonceKey());
1405     if (!proc.shouldWaitClientAck(getEnvironment())) {
1406       procInfo.setClientAckTime(0);
1407     }
1408 
1409     completed.put(procInfo.getProcId(), procInfo);
1410     rollbackStack.remove(proc.getProcId());
1411     procedures.remove(proc.getProcId());
1412 
1413     // call the runnableSet completion cleanup handler
1414     try {
1415       runnables.completionCleanup(proc);
1416     } catch (Throwable e) {
1417       // Catch NullPointerExceptions or similar errors...
1418       LOG.error("CODE-BUG: uncatched runtime exception for runnableSet: " + runnables, e);
1419     }
1420 
1421     // Notify the listeners
1422     sendProcedureFinishedNotification(proc.getProcId());
1423   }
1424 
1425   public Pair<ProcedureInfo, Procedure> getResultOrProcedure(final long procId) {
1426     ProcedureInfo result = completed.get(procId);
1427     Procedure proc = null;
1428     if (result == null) {
1429       proc = procedures.get(procId);
1430       if (proc == null) {
1431         result = completed.get(procId);
1432       }
1433     }
1434     return new Pair(result, proc);
1435   }
1436 }