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.common.annotations.VisibleForTesting;
22  
23  import java.io.FileNotFoundException;
24  import java.io.IOException;
25  import java.util.ArrayList;
26  import java.util.Arrays;
27  import java.util.Collections;
28  import java.util.Comparator;
29  import java.util.HashSet;
30  import java.util.Iterator;
31  import java.util.LinkedList;
32  import java.util.Set;
33  import java.util.concurrent.LinkedTransferQueue;
34  import java.util.concurrent.TimeUnit;
35  import java.util.concurrent.atomic.AtomicBoolean;
36  import java.util.concurrent.atomic.AtomicLong;
37  import java.util.concurrent.atomic.AtomicReference;
38  import java.util.concurrent.locks.Condition;
39  import java.util.concurrent.locks.ReentrantLock;
40  
41  import org.apache.commons.collections.buffer.CircularFifoBuffer;
42  import org.apache.commons.logging.Log;
43  import org.apache.commons.logging.LogFactory;
44  import org.apache.hadoop.conf.Configuration;
45  import org.apache.hadoop.fs.FSDataOutputStream;
46  import org.apache.hadoop.fs.FileAlreadyExistsException;
47  import org.apache.hadoop.fs.FileStatus;
48  import org.apache.hadoop.fs.FileSystem;
49  import org.apache.hadoop.fs.Path;
50  import org.apache.hadoop.fs.PathFilter;
51  import org.apache.hadoop.hbase.classification.InterfaceAudience;
52  import org.apache.hadoop.hbase.classification.InterfaceStability;
53  import org.apache.hadoop.hbase.procedure2.Procedure;
54  import org.apache.hadoop.hbase.procedure2.store.ProcedureStoreBase;
55  import org.apache.hadoop.hbase.procedure2.store.ProcedureStoreTracker;
56  import org.apache.hadoop.hbase.procedure2.util.ByteSlot;
57  import org.apache.hadoop.hbase.procedure2.util.StringUtils;
58  import org.apache.hadoop.hbase.protobuf.generated.ProcedureProtos.ProcedureWALHeader;
59  import org.apache.hadoop.hbase.util.Threads;
60  import org.apache.hadoop.ipc.RemoteException;
61  
62  /**
63   * WAL implementation of the ProcedureStore.
64   */
65  @InterfaceAudience.Private
66  @InterfaceStability.Evolving
67  public class WALProcedureStore extends ProcedureStoreBase {
68    private static final Log LOG = LogFactory.getLog(WALProcedureStore.class);
69  
70    public interface LeaseRecovery {
71      void recoverFileLease(FileSystem fs, Path path) throws IOException;
72    }
73  
74    public static final String MAX_RETRIES_BEFORE_ROLL_CONF_KEY =
75      "hbase.procedure.store.wal.max.retries.before.roll";
76    private static final int DEFAULT_MAX_RETRIES_BEFORE_ROLL = 3;
77  
78    public static final String WAIT_BEFORE_ROLL_CONF_KEY =
79      "hbase.procedure.store.wal.wait.before.roll";
80    private static final int DEFAULT_WAIT_BEFORE_ROLL = 500;
81  
82    public static final String ROLL_RETRIES_CONF_KEY =
83      "hbase.procedure.store.wal.max.roll.retries";
84    private static final int DEFAULT_ROLL_RETRIES = 3;
85  
86    public static final String MAX_SYNC_FAILURE_ROLL_CONF_KEY =
87      "hbase.procedure.store.wal.sync.failure.roll.max";
88    private static final int DEFAULT_MAX_SYNC_FAILURE_ROLL = 3;
89  
90    public static final String PERIODIC_ROLL_CONF_KEY =
91      "hbase.procedure.store.wal.periodic.roll.msec";
92    private static final int DEFAULT_PERIODIC_ROLL = 60 * 60 * 1000; // 1h
93  
94    public static final String SYNC_WAIT_MSEC_CONF_KEY = "hbase.procedure.store.wal.sync.wait.msec";
95    private static final int DEFAULT_SYNC_WAIT_MSEC = 100;
96  
97    public static final String USE_HSYNC_CONF_KEY = "hbase.procedure.store.wal.use.hsync";
98    private static final boolean DEFAULT_USE_HSYNC = true;
99  
100   public static final String ROLL_THRESHOLD_CONF_KEY = "hbase.procedure.store.wal.roll.threshold";
101   private static final long DEFAULT_ROLL_THRESHOLD = 32 * 1024 * 1024; // 32M
102 
103   public static final String STORE_WAL_SYNC_STATS_COUNT =
104       "hbase.procedure.store.wal.sync.stats.count";
105   private static final int DEFAULT_SYNC_STATS_COUNT = 10;
106 
107   private final LinkedList<ProcedureWALFile> logs = new LinkedList<ProcedureWALFile>();
108   private final ProcedureStoreTracker storeTracker = new ProcedureStoreTracker();
109   private final ReentrantLock lock = new ReentrantLock();
110   private final Condition waitCond = lock.newCondition();
111   private final Condition slotCond = lock.newCondition();
112   private final Condition syncCond = lock.newCondition();
113 
114   private final LeaseRecovery leaseRecovery;
115   private final Configuration conf;
116   private final FileSystem fs;
117   private final Path walDir;
118 
119   private final AtomicReference<Throwable> syncException = new AtomicReference<Throwable>();
120   private final AtomicBoolean loading = new AtomicBoolean(true);
121   private final AtomicBoolean inSync = new AtomicBoolean(false);
122   private final AtomicLong totalSynced = new AtomicLong(0);
123   private final AtomicLong lastRollTs = new AtomicLong(0);
124 
125   private LinkedTransferQueue<ByteSlot> slotsCache = null;
126   private Set<ProcedureWALFile> corruptedLogs = null;
127   private FSDataOutputStream stream = null;
128   private long flushLogId = 0;
129   private int slotIndex = 0;
130   private Thread syncThread;
131   private ByteSlot[] slots;
132 
133   private int maxRetriesBeforeRoll;
134   private int maxSyncFailureRoll;
135   private int waitBeforeRoll;
136   private int rollRetries;
137   private int periodicRollMsec;
138   private long rollThreshold;
139   private boolean useHsync;
140   private int syncWaitMsec;
141 
142   // Variables used for UI display
143   private CircularFifoBuffer syncMetricsBuffer;
144 
145   public static class SyncMetrics {
146     private long timestamp;
147     private long syncWaitMs;
148     private long totalSyncedBytes;
149     private int syncedEntries;
150     private float syncedPerSec;
151 
152     public long getTimestamp() {
153       return timestamp;
154     }
155 
156     public long getSyncWaitMs() {
157       return syncWaitMs;
158     }
159 
160     public long getTotalSyncedBytes() {
161       return totalSyncedBytes;
162     }
163 
164     public long getSyncedEntries() {
165       return syncedEntries;
166     }
167 
168     public float getSyncedPerSec() {
169       return syncedPerSec;
170     }
171   }
172 
173   public WALProcedureStore(final Configuration conf, final FileSystem fs, final Path walDir,
174       final LeaseRecovery leaseRecovery) {
175     this.fs = fs;
176     this.conf = conf;
177     this.walDir = walDir;
178     this.leaseRecovery = leaseRecovery;
179   }
180 
181   @Override
182   public void start(int numSlots) throws IOException {
183     if (!setRunning(true)) {
184       return;
185     }
186 
187     // Init buffer slots
188     loading.set(true);
189     slots = new ByteSlot[numSlots];
190     slotsCache = new LinkedTransferQueue();
191     while (slotsCache.size() < numSlots) {
192       slotsCache.offer(new ByteSlot());
193     }
194 
195     // Tunings
196     maxRetriesBeforeRoll =
197       conf.getInt(MAX_RETRIES_BEFORE_ROLL_CONF_KEY, DEFAULT_MAX_RETRIES_BEFORE_ROLL);
198     maxSyncFailureRoll = conf.getInt(MAX_SYNC_FAILURE_ROLL_CONF_KEY, DEFAULT_MAX_SYNC_FAILURE_ROLL);
199     waitBeforeRoll = conf.getInt(WAIT_BEFORE_ROLL_CONF_KEY, DEFAULT_WAIT_BEFORE_ROLL);
200     rollRetries = conf.getInt(ROLL_RETRIES_CONF_KEY, DEFAULT_ROLL_RETRIES);
201     rollThreshold = conf.getLong(ROLL_THRESHOLD_CONF_KEY, DEFAULT_ROLL_THRESHOLD);
202     periodicRollMsec = conf.getInt(PERIODIC_ROLL_CONF_KEY, DEFAULT_PERIODIC_ROLL);
203     syncWaitMsec = conf.getInt(SYNC_WAIT_MSEC_CONF_KEY, DEFAULT_SYNC_WAIT_MSEC);
204     useHsync = conf.getBoolean(USE_HSYNC_CONF_KEY, DEFAULT_USE_HSYNC);
205 
206     // WebUI
207     syncMetricsBuffer = new CircularFifoBuffer(
208       conf.getInt(STORE_WAL_SYNC_STATS_COUNT, DEFAULT_SYNC_STATS_COUNT));
209 
210     // Init sync thread
211     syncThread = new Thread("WALProcedureStoreSyncThread") {
212       @Override
213       public void run() {
214         try {
215           syncLoop();
216         } catch (Throwable e) {
217           LOG.error("Got an exception from the sync-loop", e);
218           if (!isSyncAborted()) {
219             sendAbortProcessSignal();
220           }
221         }
222       }
223     };
224     syncThread.start();
225   }
226 
227   @Override
228   public void stop(boolean abort) {
229     if (!setRunning(false)) {
230       return;
231     }
232 
233     LOG.info("Stopping the WAL Procedure Store");
234     sendStopSignal();
235 
236     if (!abort) {
237       try {
238         while (syncThread.isAlive()) {
239           sendStopSignal();
240           syncThread.join(250);
241         }
242       } catch (InterruptedException e) {
243         LOG.warn("join interrupted", e);
244         Thread.currentThread().interrupt();
245       }
246     }
247 
248     // Close the writer
249     closeStream();
250 
251     // Close the old logs
252     // they should be already closed, this is just in case the load fails
253     // and we call start() and then stop()
254     for (ProcedureWALFile log: logs) {
255       log.close();
256     }
257     logs.clear();
258   }
259 
260   private void sendStopSignal() {
261     if (lock.tryLock()) {
262       try {
263         waitCond.signalAll();
264         syncCond.signalAll();
265       } finally {
266         lock.unlock();
267       }
268     }
269   }
270 
271   @Override
272   public int getNumThreads() {
273     return slots == null ? 0 : slots.length;
274   }
275 
276   public ProcedureStoreTracker getStoreTracker() {
277     return storeTracker;
278   }
279 
280   public ArrayList<ProcedureWALFile> getActiveLogs() {
281     lock.lock();
282     try {
283       return new ArrayList<ProcedureWALFile>(logs);
284     } finally {
285       lock.unlock();
286     }
287   }
288 
289   public Set<ProcedureWALFile> getCorruptedLogs() {
290     return corruptedLogs;
291   }
292 
293   @Override
294   public void recoverLease() throws IOException {
295     lock.lock();
296     try {
297       LOG.info("Starting WAL Procedure Store lease recovery");
298       FileStatus[] oldLogs = getLogFiles();
299       while (isRunning()) {
300         // Get Log-MaxID and recover lease on old logs
301         try {
302           flushLogId = initOldLogs(oldLogs);
303         } catch (FileNotFoundException e) {
304           LOG.warn("someone else is active and deleted logs. retrying.", e);
305           oldLogs = getLogFiles();
306           continue;
307         }
308 
309         // Create new state-log
310         if (!rollWriter(flushLogId + 1)) {
311           // someone else has already created this log
312           LOG.debug("someone else has already created log " + flushLogId);
313           continue;
314         }
315 
316         // We have the lease on the log
317         oldLogs = getLogFiles();
318         if (getMaxLogId(oldLogs) > flushLogId) {
319           if (LOG.isDebugEnabled()) {
320             LOG.debug("Someone else created new logs. Expected maxLogId < " + flushLogId);
321           }
322           logs.getLast().removeFile();
323           continue;
324         }
325 
326         LOG.info("Lease acquired for flushLogId: " + flushLogId);
327         break;
328       }
329     } finally {
330       lock.unlock();
331     }
332   }
333 
334   @Override
335   public void load(final ProcedureLoader loader) throws IOException {
336     if (logs.isEmpty()) {
337       throw new RuntimeException("recoverLease() must be called before loading data");
338     }
339 
340     // Nothing to do, If we have only the current log.
341     if (logs.size() == 1) {
342       if (LOG.isDebugEnabled()) {
343         LOG.debug("No state logs to replay.");
344       }
345       loader.setMaxProcId(0);
346       loading.set(false);
347       return;
348     }
349 
350     // Load the old logs
351     Iterator<ProcedureWALFile> it = logs.descendingIterator();
352     it.next(); // Skip the current log
353     try {
354       ProcedureWALFormat.load(it, storeTracker, new ProcedureWALFormat.Loader() {
355         @Override
356         public void setMaxProcId(long maxProcId) {
357           loader.setMaxProcId(maxProcId);
358         }
359 
360         @Override
361         public void load(ProcedureIterator procIter) throws IOException {
362           loader.load(procIter);
363         }
364 
365         @Override
366         public void handleCorrupted(ProcedureIterator procIter) throws IOException {
367           loader.handleCorrupted(procIter);
368         }
369 
370         @Override
371         public void markCorruptedWAL(ProcedureWALFile log, IOException e) {
372           if (corruptedLogs == null) {
373             corruptedLogs = new HashSet<ProcedureWALFile>();
374           }
375           corruptedLogs.add(log);
376           // TODO: sideline corrupted log
377         }
378       });
379     } finally {
380       loading.set(false);
381     }
382   }
383 
384   @Override
385   public void insert(final Procedure proc, final Procedure[] subprocs) {
386     if (LOG.isTraceEnabled()) {
387       LOG.trace("Insert " + proc + ", subproc=" + Arrays.toString(subprocs));
388     }
389 
390     ByteSlot slot = acquireSlot();
391     try {
392       // Serialize the insert
393       long[] subProcIds = null;
394       if (subprocs != null) {
395         ProcedureWALFormat.writeInsert(slot, proc, subprocs);
396         subProcIds = new long[subprocs.length];
397         for (int i = 0; i < subprocs.length; ++i) {
398           subProcIds[i] = subprocs[i].getProcId();
399         }
400       } else {
401         assert !proc.hasParent();
402         ProcedureWALFormat.writeInsert(slot, proc);
403       }
404 
405       // Push the transaction data and wait until it is persisted
406       pushData(PushType.INSERT, slot, proc.getProcId(), subProcIds);
407     } catch (IOException e) {
408       // We are not able to serialize the procedure.
409       // this is a code error, and we are not able to go on.
410       LOG.fatal("Unable to serialize one of the procedure: proc=" + proc +
411                 ", subprocs=" + Arrays.toString(subprocs), e);
412       throw new RuntimeException(e);
413     } finally {
414       releaseSlot(slot);
415     }
416   }
417 
418   @Override
419   public void update(final Procedure proc) {
420     if (LOG.isTraceEnabled()) {
421       LOG.trace("Update " + proc);
422     }
423 
424     ByteSlot slot = acquireSlot();
425     try {
426       // Serialize the update
427       ProcedureWALFormat.writeUpdate(slot, proc);
428 
429       // Push the transaction data and wait until it is persisted
430       pushData(PushType.UPDATE, slot, proc.getProcId(), null);
431     } catch (IOException e) {
432       // We are not able to serialize the procedure.
433       // this is a code error, and we are not able to go on.
434       LOG.fatal("Unable to serialize the procedure: " + proc, e);
435       throw new RuntimeException(e);
436     } finally {
437       releaseSlot(slot);
438     }
439   }
440 
441   @Override
442   public void delete(final long procId) {
443     if (LOG.isTraceEnabled()) {
444       LOG.trace("Delete " + procId);
445     }
446 
447     ByteSlot slot = acquireSlot();
448     try {
449       // Serialize the delete
450       ProcedureWALFormat.writeDelete(slot, procId);
451 
452       // Push the transaction data and wait until it is persisted
453       pushData(PushType.DELETE, slot, procId, null);
454     } catch (IOException e) {
455       // We are not able to serialize the procedure.
456       // this is a code error, and we are not able to go on.
457       LOG.fatal("Unable to serialize the procedure: " + procId, e);
458       throw new RuntimeException(e);
459     } finally {
460       releaseSlot(slot);
461     }
462   }
463 
464   @Override
465   public void delete(final Procedure proc, final long[] subProcIds) {
466     if (LOG.isTraceEnabled()) {
467       LOG.trace("Update " + proc + " and Delete " + Arrays.toString(subProcIds));
468     }
469 
470     ByteSlot slot = acquireSlot();
471     try {
472       // Serialize the delete
473       ProcedureWALFormat.writeDelete(slot, proc, subProcIds);
474 
475       // Push the transaction data and wait until it is persisted
476       pushData(PushType.DELETE, slot, proc.getProcId(), subProcIds);
477     } catch (IOException e) {
478       // We are not able to serialize the procedure.
479       // this is a code error, and we are not able to go on.
480       LOG.fatal("Unable to serialize the procedure: " + proc, e);
481       throw new RuntimeException(e);
482     } finally {
483       releaseSlot(slot);
484     }
485   }
486 
487   private ByteSlot acquireSlot() {
488     ByteSlot slot = slotsCache.poll();
489     return slot != null ? slot : new ByteSlot();
490   }
491 
492   private void releaseSlot(final ByteSlot slot) {
493     slot.reset();
494     slotsCache.offer(slot);
495   }
496 
497   private enum PushType { INSERT, UPDATE, DELETE };
498 
499   private long pushData(final PushType type, final ByteSlot slot,
500       final long procId, final long[] subProcIds) {
501     if (!isRunning()) {
502       throw new RuntimeException("the store must be running before inserting data");
503     }
504     if (logs.isEmpty()) {
505       throw new RuntimeException("recoverLease() must be called before inserting data");
506     }
507 
508     long logId = -1;
509     lock.lock();
510     try {
511       // Wait for the sync to be completed
512       while (true) {
513         if (!isRunning()) {
514           throw new RuntimeException("store no longer running");
515         } else if (isSyncAborted()) {
516           throw new RuntimeException("sync aborted", syncException.get());
517         } else if (inSync.get()) {
518           syncCond.await();
519         } else if (slotIndex == slots.length) {
520           slotCond.signal();
521           syncCond.await();
522         } else {
523           break;
524         }
525       }
526 
527       updateStoreTracker(type, procId, subProcIds);
528       slots[slotIndex++] = slot;
529       logId = flushLogId;
530 
531       // Notify that there is new data
532       if (slotIndex == 1) {
533         waitCond.signal();
534       }
535 
536       // Notify that the slots are full
537       if (slotIndex == slots.length) {
538         waitCond.signal();
539         slotCond.signal();
540       }
541 
542       syncCond.await();
543     } catch (InterruptedException e) {
544       Thread.currentThread().interrupt();
545       sendAbortProcessSignal();
546       throw new RuntimeException(e);
547     } finally {
548       lock.unlock();
549       if (isSyncAborted()) {
550         throw new RuntimeException("sync aborted", syncException.get());
551       }
552     }
553     return logId;
554   }
555 
556   private void updateStoreTracker(final PushType type,
557       final long procId, final long[] subProcIds) {
558     switch (type) {
559       case INSERT:
560         if (subProcIds == null) {
561           storeTracker.insert(procId);
562         } else {
563           storeTracker.insert(procId, subProcIds);
564         }
565         break;
566       case UPDATE:
567         storeTracker.update(procId);
568         break;
569       case DELETE:
570         if (subProcIds != null && subProcIds.length > 0) {
571           storeTracker.delete(subProcIds);
572         } else {
573           storeTracker.delete(procId);
574         }
575         break;
576       default:
577         throw new RuntimeException("invalid push type " + type);
578     }
579   }
580 
581   private boolean isSyncAborted() {
582     return syncException.get() != null;
583   }
584 
585   private void syncLoop() throws Throwable {
586     long totalSyncedToStore = 0;
587     inSync.set(false);
588     lock.lock();
589     try {
590       while (isRunning()) {
591         try {
592           // Wait until new data is available
593           if (slotIndex == 0) {
594             if (!loading.get()) {
595               periodicRoll();
596             }
597 
598             if (LOG.isTraceEnabled()) {
599               float rollTsSec = getMillisFromLastRoll() / 1000.0f;
600               LOG.trace(String.format("Waiting for data. flushed=%s (%s/sec)",
601                         StringUtils.humanSize(totalSynced.get()),
602                         StringUtils.humanSize(totalSynced.get() / rollTsSec)));
603             }
604 
605             waitCond.await(getMillisToNextPeriodicRoll(), TimeUnit.MILLISECONDS);
606             if (slotIndex == 0) {
607               // no data.. probably a stop() or a periodic roll
608               continue;
609             }
610           }
611           // Wait SYNC_WAIT_MSEC or the signal of "slots full" before flushing
612           final long syncWaitSt = System.currentTimeMillis();
613           if (slotIndex != slots.length) {
614             slotCond.await(syncWaitMsec, TimeUnit.MILLISECONDS);
615           }
616 
617           final long currentTs = System.currentTimeMillis();
618           final long syncWaitMs = currentTs - syncWaitSt;
619           final float rollSec = getMillisFromLastRoll() / 1000.0f;
620           final float syncedPerSec = totalSyncedToStore / rollSec;
621           if (LOG.isTraceEnabled() && (syncWaitMs > 10 || slotIndex < slots.length)) {
622             LOG.trace(String.format("Sync wait %s, slotIndex=%s , totalSynced=%s (%s/sec)",
623                       StringUtils.humanTimeDiff(syncWaitMs), slotIndex,
624                       StringUtils.humanSize(totalSyncedToStore),
625                       StringUtils.humanSize(syncedPerSec)));
626           }
627 
628           // update webui circular buffers (TODO: get rid of allocations)
629           final SyncMetrics syncMetrics = new SyncMetrics();
630           syncMetrics.timestamp = currentTs;
631           syncMetrics.syncWaitMs = syncWaitMs;
632           syncMetrics.syncedEntries = slotIndex;
633           syncMetrics.totalSyncedBytes = totalSyncedToStore;
634           syncMetrics.syncedPerSec = syncedPerSec;
635           syncMetricsBuffer.add(syncMetrics);
636 
637           // sync
638           inSync.set(true);
639           long slotSize = syncSlots();
640           logs.getLast().addToSize(slotSize);
641           totalSyncedToStore = totalSynced.addAndGet(slotSize);
642           slotIndex = 0;
643           inSync.set(false);
644         } catch (InterruptedException e) {
645           Thread.currentThread().interrupt();
646           sendAbortProcessSignal();
647           syncException.compareAndSet(null, e);
648           throw e;
649         } catch (Throwable t) {
650           syncException.compareAndSet(null, t);
651           throw t;
652         } finally {
653           syncCond.signalAll();
654         }
655       }
656     } finally {
657       lock.unlock();
658     }
659   }
660 
661   public ArrayList<SyncMetrics> getSyncMetrics() {
662     lock.lock();
663     try {
664       return new ArrayList<SyncMetrics>(syncMetricsBuffer);
665     } finally {
666       lock.unlock();
667     }
668   }
669 
670   private long syncSlots() throws Throwable {
671     int retry = 0;
672     int logRolled = 0;
673     long totalSynced = 0;
674     do {
675       try {
676         totalSynced = syncSlots(stream, slots, 0, slotIndex);
677         break;
678       } catch (Throwable e) {
679         LOG.warn("unable to sync slots, retry=" + retry);
680         if (++retry >= maxRetriesBeforeRoll) {
681           if (logRolled >= maxSyncFailureRoll) {
682             LOG.error("Sync slots after log roll failed, abort.", e);
683             sendAbortProcessSignal();
684             throw e;
685           }
686 
687           if (!rollWriterOrDie()) {
688             throw e;
689           }
690 
691           logRolled++;
692           retry = 0;
693         }
694       }
695     } while (isRunning());
696     return totalSynced;
697   }
698 
699   protected long syncSlots(FSDataOutputStream stream, ByteSlot[] slots, int offset, int count)
700       throws IOException {
701     long totalSynced = 0;
702     for (int i = 0; i < count; ++i) {
703       ByteSlot data = slots[offset + i];
704       data.writeTo(stream);
705       totalSynced += data.size();
706     }
707 
708     if (useHsync) {
709       stream.hsync();
710     } else {
711       stream.hflush();
712     }
713     sendPostSyncSignal();
714 
715     if (LOG.isTraceEnabled()) {
716       LOG.trace("Sync slots=" + count + '/' + slots.length +
717                 ", flushed=" + StringUtils.humanSize(totalSynced));
718     }
719     return totalSynced;
720   }
721 
722   private boolean rollWriterOrDie() {
723     for (int i = 0; i < rollRetries; ++i) {
724       if (i > 0) Threads.sleepWithoutInterrupt(waitBeforeRoll * i);
725 
726       try {
727         if (rollWriter()) {
728           return true;
729         }
730       } catch (IOException e) {
731         LOG.warn("Unable to roll the log, attempt=" + (i + 1), e);
732       }
733     }
734     LOG.fatal("Unable to roll the log");
735     sendAbortProcessSignal();
736     throw new RuntimeException("unable to roll the log");
737   }
738 
739   private boolean tryRollWriter() {
740     try {
741       return rollWriter();
742     } catch (IOException e) {
743       LOG.warn("Unable to roll the log", e);
744       return false;
745     }
746   }
747 
748   public long getMillisToNextPeriodicRoll() {
749     if (lastRollTs.get() > 0 && periodicRollMsec > 0) {
750       return periodicRollMsec - getMillisFromLastRoll();
751     }
752     return Long.MAX_VALUE;
753   }
754 
755   public long getMillisFromLastRoll() {
756     return (System.currentTimeMillis() - lastRollTs.get());
757   }
758 
759   @VisibleForTesting
760   protected void periodicRollForTesting() throws IOException {
761     lock.lock();
762     try {
763       periodicRoll();
764     } finally {
765       lock.unlock();
766     }
767   }
768 
769   @VisibleForTesting
770   protected boolean rollWriterForTesting() throws IOException {
771     lock.lock();
772     try {
773       return rollWriter();
774     } finally {
775       lock.unlock();
776     }
777   }
778 
779   private void periodicRoll() throws IOException {
780     if (storeTracker.isEmpty()) {
781       if (LOG.isTraceEnabled()) {
782         LOG.trace("no active procedures");
783       }
784       tryRollWriter();
785       removeAllLogs(flushLogId - 1);
786     } else {
787       if (storeTracker.isUpdated()) {
788         if (LOG.isTraceEnabled()) {
789           LOG.trace("all the active procedures are in the latest log");
790         }
791         removeAllLogs(flushLogId - 1);
792       }
793 
794       // if the log size has exceeded the roll threshold
795       // or the periodic roll timeout is expired, try to roll the wal.
796       if (totalSynced.get() > rollThreshold || getMillisToNextPeriodicRoll() <= 0) {
797         tryRollWriter();
798       }
799 
800       removeInactiveLogs();
801     }
802   }
803 
804   private boolean rollWriter() throws IOException {
805     // Create new state-log
806     if (!rollWriter(flushLogId + 1)) {
807       LOG.warn("someone else has already created log " + flushLogId);
808       return false;
809     }
810 
811     // We have the lease on the log,
812     // but we should check if someone else has created new files
813     if (getMaxLogId(getLogFiles()) > flushLogId) {
814       LOG.warn("Someone else created new logs. Expected maxLogId < " + flushLogId);
815       logs.getLast().removeFile();
816       return false;
817     }
818 
819     // We have the lease on the log
820     return true;
821   }
822 
823   private boolean rollWriter(final long logId) throws IOException {
824     assert logId > flushLogId : "logId=" + logId + " flushLogId=" + flushLogId;
825     assert lock.isHeldByCurrentThread() : "expected to be the lock owner. " + lock.isLocked();
826 
827     ProcedureWALHeader header = ProcedureWALHeader.newBuilder()
828       .setVersion(ProcedureWALFormat.HEADER_VERSION)
829       .setType(ProcedureWALFormat.LOG_TYPE_STREAM)
830       .setMinProcId(storeTracker.getMinProcId())
831       .setLogId(logId)
832       .build();
833 
834     FSDataOutputStream newStream = null;
835     Path newLogFile = null;
836     long startPos = -1;
837     newLogFile = getLogFilePath(logId);
838     try {
839       newStream = fs.create(newLogFile, false);
840     } catch (FileAlreadyExistsException e) {
841       LOG.error("Log file with id=" + logId + " already exists", e);
842       return false;
843     } catch (RemoteException re) {
844       LOG.warn("failed to create log file with id=" + logId, re);
845       return false;
846     }
847     try {
848       ProcedureWALFormat.writeHeader(newStream, header);
849       startPos = newStream.getPos();
850     } catch (IOException ioe) {
851       LOG.warn("Encountered exception writing header", ioe);
852       newStream.close();
853       return false;
854     }
855 
856     closeStream();
857 
858     storeTracker.resetUpdates();
859     stream = newStream;
860     flushLogId = logId;
861     totalSynced.set(0);
862     long rollTs = System.currentTimeMillis();
863     lastRollTs.set(rollTs);
864     logs.add(new ProcedureWALFile(fs, newLogFile, header, startPos, rollTs));
865 
866     if (LOG.isDebugEnabled()) {
867       LOG.debug("Roll new state log: " + logId);
868     }
869     return true;
870   }
871 
872   private void closeStream() {
873     try {
874       if (stream != null) {
875         try {
876           ProcedureWALFile log = logs.getLast();
877           log.setProcIds(storeTracker.getUpdatedMinProcId(), storeTracker.getUpdatedMaxProcId());
878           long trailerSize = ProcedureWALFormat.writeTrailer(stream, storeTracker);
879           log.addToSize(trailerSize);
880         } catch (IOException e) {
881           LOG.warn("Unable to write the trailer: " + e.getMessage());
882         }
883         stream.close();
884       }
885     } catch (IOException e) {
886       LOG.error("Unable to close the stream", e);
887     } finally {
888       stream = null;
889     }
890   }
891 
892   // ==========================================================================
893   //  Log Files cleaner helpers
894   // ==========================================================================
895   private void removeInactiveLogs() {
896     // Verify if the ProcId of the first oldest is still active. if not remove the file.
897     while (logs.size() > 1) {
898       ProcedureWALFile log = logs.getFirst();
899       if (storeTracker.isTracking(log.getMinProcId(), log.getMaxProcId())) {
900         break;
901       }
902       removeLogFile(log);
903     }
904   }
905 
906   private void removeAllLogs(long lastLogId) {
907     if (logs.size() <= 1) return;
908 
909     if (LOG.isDebugEnabled()) {
910       LOG.debug("Remove all state logs with ID less than " + lastLogId);
911     }
912     while (logs.size() > 1) {
913       ProcedureWALFile log = logs.getFirst();
914       if (lastLogId < log.getLogId()) {
915         break;
916       }
917       removeLogFile(log);
918     }
919   }
920 
921   private boolean removeLogFile(final ProcedureWALFile log) {
922     try {
923       if (LOG.isTraceEnabled()) {
924         LOG.trace("Removing log=" + log);
925       }
926       log.removeFile();
927       logs.remove(log);
928       if (LOG.isDebugEnabled()) {
929         LOG.info("Removed log=" + log + " activeLogs=" + logs);
930       }
931       assert logs.size() > 0 : "expected at least one log";
932     } catch (IOException e) {
933       LOG.error("Unable to remove log: " + log, e);
934       return false;
935     }
936     return true;
937   }
938 
939   // ==========================================================================
940   //  FileSystem Log Files helpers
941   // ==========================================================================
942   public Path getWALDir() {
943     return this.walDir;
944   }
945 
946   public FileSystem getFileSystem() {
947     return this.fs;
948   }
949 
950   protected Path getLogFilePath(final long logId) throws IOException {
951     return new Path(walDir, String.format("state-%020d.log", logId));
952   }
953 
954   private static long getLogIdFromName(final String name) {
955     int end = name.lastIndexOf(".log");
956     int start = name.lastIndexOf('-') + 1;
957     while (start < end) {
958       if (name.charAt(start) != '0')
959         break;
960       start++;
961     }
962     return Long.parseLong(name.substring(start, end));
963   }
964 
965   private static final PathFilter WALS_PATH_FILTER = new PathFilter() {
966     @Override
967     public boolean accept(Path path) {
968       String name = path.getName();
969       return name.startsWith("state-") && name.endsWith(".log");
970     }
971   };
972 
973   private static final Comparator<FileStatus> FILE_STATUS_ID_COMPARATOR =
974       new Comparator<FileStatus>() {
975     @Override
976     public int compare(FileStatus a, FileStatus b) {
977       final long aId = getLogIdFromName(a.getPath().getName());
978       final long bId = getLogIdFromName(b.getPath().getName());
979       return Long.compare(aId, bId);
980     }
981   };
982 
983   private FileStatus[] getLogFiles() throws IOException {
984     try {
985       FileStatus[] files = fs.listStatus(walDir, WALS_PATH_FILTER);
986       Arrays.sort(files, FILE_STATUS_ID_COMPARATOR);
987       return files;
988     } catch (FileNotFoundException e) {
989       LOG.warn("Log directory not found: " + e.getMessage());
990       return null;
991     }
992   }
993 
994   private static long getMaxLogId(final FileStatus[] logFiles) {
995     long maxLogId = 0;
996     if (logFiles != null && logFiles.length > 0) {
997       for (int i = 0; i < logFiles.length; ++i) {
998         maxLogId = Math.max(maxLogId, getLogIdFromName(logFiles[i].getPath().getName()));
999       }
1000     }
1001     return maxLogId;
1002   }
1003 
1004   /**
1005    * @return Max-LogID of the specified log file set
1006    */
1007   private long initOldLogs(final FileStatus[] logFiles) throws IOException {
1008     this.logs.clear();
1009 
1010     long maxLogId = 0;
1011     if (logFiles != null && logFiles.length > 0) {
1012       for (int i = 0; i < logFiles.length; ++i) {
1013         final Path logPath = logFiles[i].getPath();
1014         leaseRecovery.recoverFileLease(fs, logPath);
1015         maxLogId = Math.max(maxLogId, getLogIdFromName(logPath.getName()));
1016 
1017         ProcedureWALFile log = initOldLog(logFiles[i]);
1018         if (log != null) {
1019           this.logs.add(log);
1020         }
1021       }
1022       Collections.sort(this.logs);
1023       initTrackerFromOldLogs();
1024     }
1025     return maxLogId;
1026   }
1027 
1028   private void initTrackerFromOldLogs() {
1029     // TODO: Load the most recent tracker available
1030     if (!logs.isEmpty()) {
1031       ProcedureWALFile log = logs.getLast();
1032       try {
1033         log.readTracker(storeTracker);
1034       } catch (IOException e) {
1035         LOG.warn("Unable to read tracker for " + log + " - " + e.getMessage());
1036         // try the next one...
1037         storeTracker.reset();
1038         storeTracker.setPartialFlag(true);
1039       }
1040     }
1041   }
1042 
1043   private ProcedureWALFile initOldLog(final FileStatus logFile) throws IOException {
1044     ProcedureWALFile log = new ProcedureWALFile(fs, logFile);
1045     if (logFile.getLen() == 0) {
1046       LOG.warn("Remove uninitialized log: " + logFile);
1047       log.removeFile();
1048       return null;
1049     }
1050     if (LOG.isDebugEnabled()) {
1051       LOG.debug("Opening state-log: " + logFile);
1052     }
1053     try {
1054       log.open();
1055     } catch (ProcedureWALFormat.InvalidWALDataException e) {
1056       LOG.warn("Remove uninitialized log: " + logFile, e);
1057       log.removeFile();
1058       return null;
1059     } catch (IOException e) {
1060       String msg = "Unable to read state log: " + logFile;
1061       LOG.error(msg, e);
1062       throw new IOException(msg, e);
1063     }
1064 
1065     if (log.isCompacted()) {
1066       try {
1067         log.readTrailer();
1068       } catch (IOException e) {
1069         LOG.warn("Unfinished compacted log: " + logFile, e);
1070         log.removeFile();
1071         return null;
1072       }
1073     }
1074     return log;
1075   }
1076 }