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  package org.apache.hadoop.hbase.util;
19  
20  import java.io.FileNotFoundException;
21  import java.io.IOException;
22  import java.util.Comparator;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.TreeMap;
26  import java.util.concurrent.ConcurrentHashMap;
27  import java.util.regex.Matcher;
28  import java.util.regex.Pattern;
29  
30  import org.apache.commons.lang.NotImplementedException;
31  import org.apache.commons.logging.Log;
32  import org.apache.commons.logging.LogFactory;
33  import org.apache.hadoop.classification.InterfaceAudience;
34  import org.apache.hadoop.conf.Configuration;
35  import org.apache.hadoop.fs.FSDataInputStream;
36  import org.apache.hadoop.fs.FSDataOutputStream;
37  import org.apache.hadoop.fs.FileStatus;
38  import org.apache.hadoop.fs.FileSystem;
39  import org.apache.hadoop.fs.Path;
40  import org.apache.hadoop.fs.PathFilter;
41  import org.apache.hadoop.hbase.TableName;
42  import org.apache.hadoop.hbase.exceptions.DeserializationException;
43  import org.apache.hadoop.hbase.HConstants;
44  import org.apache.hadoop.hbase.HTableDescriptor;
45  import org.apache.hadoop.hbase.TableDescriptors;
46  import org.apache.hadoop.hbase.TableInfoMissingException;
47  import org.apache.hadoop.hbase.protobuf.ProtobufUtil;
48  
49  import com.google.common.annotations.VisibleForTesting;
50  import com.google.common.primitives.Ints;
51  
52  
53  /**
54   * Implementation of {@link TableDescriptors} that reads descriptors from the
55   * passed filesystem.  It expects descriptors to be in a file in the
56   * {@link #TABLEINFO_DIR} subdir of the table's directory in FS.  Can be read-only
57   *  -- i.e. does not modify the filesystem or can be read and write.
58   * 
59   * <p>Also has utility for keeping up the table descriptors tableinfo file.
60   * The table schema file is kept in the {@link #TABLEINFO_DIR} subdir
61   * of the table directory in the filesystem.
62   * It has a {@link #TABLEINFO_FILE_PREFIX} and then a suffix that is the
63   * edit sequenceid: e.g. <code>.tableinfo.0000000003</code>.  This sequenceid
64   * is always increasing.  It starts at zero.  The table schema file with the
65   * highest sequenceid has the most recent schema edit. Usually there is one file
66   * only, the most recent but there may be short periods where there are more
67   * than one file. Old files are eventually cleaned.  Presumption is that there
68   * will not be lots of concurrent clients making table schema edits.  If so,
69   * the below needs a bit of a reworking and perhaps some supporting api in hdfs.
70   */
71  @InterfaceAudience.Private
72  public class FSTableDescriptors implements TableDescriptors {
73    private static final Log LOG = LogFactory.getLog(FSTableDescriptors.class);
74    private final FileSystem fs;
75    private final Path rootdir;
76    private final boolean fsreadonly;
77    @VisibleForTesting long cachehits = 0;
78    @VisibleForTesting long invocations = 0;
79  
80    /** The file name prefix used to store HTD in HDFS  */
81    static final String TABLEINFO_FILE_PREFIX = ".tableinfo";
82    static final String TABLEINFO_DIR = ".tabledesc";
83    static final String TMP_DIR = ".tmp";
84  
85    // This cache does not age out the old stuff.  Thinking is that the amount
86    // of data we keep up in here is so small, no need to do occasional purge.
87    // TODO.
88    private final Map<TableName, TableDescriptorAndModtime> cache =
89      new ConcurrentHashMap<TableName, TableDescriptorAndModtime>();
90  
91    /**
92     * Data structure to hold modification time and table descriptor.
93     */
94    private static class TableDescriptorAndModtime {
95      private final HTableDescriptor htd;
96      private final long modtime;
97  
98      TableDescriptorAndModtime(final long modtime, final HTableDescriptor htd) {
99        this.htd = htd;
100       this.modtime = modtime;
101     }
102 
103     long getModtime() {
104       return this.modtime;
105     }
106 
107     HTableDescriptor getTableDescriptor() {
108       return this.htd;
109     }
110   }
111 
112   /**
113    * Construct a FSTableDescriptors instance using the hbase root dir of the given
114    * conf and the filesystem where that root dir lives.
115    * This instance can do write operations (is not read only).
116    */
117   public FSTableDescriptors(final Configuration conf) throws IOException {
118     this(FSUtils.getCurrentFileSystem(conf), FSUtils.getRootDir(conf));
119   }
120   
121   public FSTableDescriptors(final FileSystem fs, final Path rootdir) {
122     this(fs, rootdir, false);
123   }
124 
125   /**
126    * @param fsreadonly True if we are read-only when it comes to filesystem
127    * operations; i.e. on remove, we do not do delete in fs.
128    */
129   public FSTableDescriptors(final FileSystem fs,
130       final Path rootdir, final boolean fsreadonly) {
131     super();
132     this.fs = fs;
133     this.rootdir = rootdir;
134     this.fsreadonly = fsreadonly;
135   }
136 
137   /**
138    * Get the current table descriptor for the given table, or null if none exists.
139    * 
140    * Uses a local cache of the descriptor but still checks the filesystem on each call
141    * to see if a newer file has been created since the cached one was read.
142    */
143   @Override
144   public HTableDescriptor get(final TableName tablename)
145   throws IOException {
146     invocations++;
147     if (HTableDescriptor.ROOT_TABLEDESC.getTableName().equals(tablename)) {
148       cachehits++;
149       return HTableDescriptor.ROOT_TABLEDESC;
150     }
151     if (HTableDescriptor.META_TABLEDESC.getTableName().equals(tablename)) {
152       cachehits++;
153       return HTableDescriptor.META_TABLEDESC;
154     }
155     // .META. and -ROOT- is already handled. If some one tries to get the descriptor for
156     // .logs, .oldlogs or .corrupt throw an exception.
157     if (HConstants.HBASE_NON_USER_TABLE_DIRS.contains(tablename.getNameAsString())) {
158        throw new IOException("No descriptor found for non table = " + tablename);
159     }
160 
161     // Look in cache of descriptors.
162     TableDescriptorAndModtime cachedtdm = this.cache.get(tablename);
163 
164     if (cachedtdm != null) {
165       // Check mod time has not changed (this is trip to NN).
166       if (getTableInfoModtime(tablename) <= cachedtdm.getModtime()) {
167         cachehits++;
168         return cachedtdm.getTableDescriptor();
169       }
170     }
171     
172     TableDescriptorAndModtime tdmt = null;
173     try {
174       tdmt = getTableDescriptorAndModtime(tablename);
175     } catch (NullPointerException e) {
176       LOG.debug("Exception during readTableDecriptor. Current table name = "
177           + tablename, e);
178     } catch (IOException ioe) {
179       LOG.debug("Exception during readTableDecriptor. Current table name = "
180           + tablename, ioe);
181     }
182     
183     if (tdmt == null) {
184       LOG.warn("The following folder is in HBase's root directory and " +
185         "doesn't contain a table descriptor, " +
186         "do consider deleting it: " + tablename);
187     } else {
188       this.cache.put(tablename, tdmt);
189     }
190     return tdmt == null ? null : tdmt.getTableDescriptor();
191   }
192 
193   /**
194    * Returns a map from table name to table descriptor for all tables.
195    */
196   @Override
197   public Map<String, HTableDescriptor> getAll()
198   throws IOException {
199     Map<String, HTableDescriptor> htds = new TreeMap<String, HTableDescriptor>();
200     List<Path> tableDirs = FSUtils.getTableDirs(fs, rootdir);
201     for (Path d: tableDirs) {
202       HTableDescriptor htd = null;
203       try {
204         htd = get(FSUtils.getTableName(d));
205       } catch (FileNotFoundException fnfe) {
206         // inability of retrieving one HTD shouldn't stop getting the remaining
207         LOG.warn("Trouble retrieving htd", fnfe);
208       }
209       if (htd == null) continue;
210       htds.put(htd.getTableName().getNameAsString(), htd);
211     }
212     return htds;
213   }
214 
215   /* (non-Javadoc)
216    * @see org.apache.hadoop.hbase.TableDescriptors#getTableDescriptors(org.apache.hadoop.fs.FileSystem, org.apache.hadoop.fs.Path)
217    */
218   @Override
219   public Map<String, HTableDescriptor> getByNamespace(String name)
220   throws IOException {
221     Map<String, HTableDescriptor> htds = new TreeMap<String, HTableDescriptor>();
222     List<Path> tableDirs =
223         FSUtils.getLocalTableDirs(fs, FSUtils.getNamespaceDir(rootdir, name));
224     for (Path d: tableDirs) {
225       HTableDescriptor htd = null;
226       try {
227         htd = get(FSUtils.getTableName(d));
228       } catch (FileNotFoundException fnfe) {
229         // inability of retrieving one HTD shouldn't stop getting the remaining
230         LOG.warn("Trouble retrieving htd", fnfe);
231       }
232       if (htd == null) continue;
233       htds.put(FSUtils.getTableName(d).getNameAsString(), htd);
234     }
235     return htds;
236   }
237 
238   /**
239    * Adds (or updates) the table descriptor to the FileSystem
240    * and updates the local cache with it.
241    */
242   @Override
243   public void add(HTableDescriptor htd) throws IOException {
244     if (fsreadonly) {
245       throw new NotImplementedException("Cannot add a table descriptor - in read only mode");
246     }
247     if (TableName.META_TABLE_NAME.equals(htd.getTableName())) {
248       throw new NotImplementedException();
249     }
250     if (HConstants.HBASE_NON_USER_TABLE_DIRS.contains(htd.getTableName().getNameAsString())) {
251       throw new NotImplementedException(
252         "Cannot add a table descriptor for a reserved subdirectory name: " + htd.getNameAsString());
253     }
254     updateTableDescriptor(htd);
255     long modtime = getTableInfoModtime(htd.getTableName());
256     this.cache.put(htd.getTableName(), new TableDescriptorAndModtime(modtime, htd));
257   }
258 
259   /**
260    * Removes the table descriptor from the local cache and returns it.
261    * If not in read only mode, it also deletes the entire table directory(!)
262    * from the FileSystem.
263    */
264   @Override
265   public HTableDescriptor remove(final TableName tablename)
266   throws IOException {
267     if (fsreadonly) {
268       throw new NotImplementedException("Cannot remove a table descriptor - in read only mode");
269     }
270     Path tabledir = getTableDir(tablename);
271     if (this.fs.exists(tabledir)) {
272       if (!this.fs.delete(tabledir, true)) {
273         throw new IOException("Failed delete of " + tabledir.toString());
274       }
275     }
276     TableDescriptorAndModtime tdm = this.cache.remove(tablename);
277     return tdm == null ? null : tdm.getTableDescriptor();
278   }
279 
280   /**
281    * Checks if a current table info file exists for the given table
282    * 
283    * @param tableName name of table
284    * @return true if exists
285    * @throws IOException
286    */
287   public boolean isTableInfoExists(TableName tableName) throws IOException {
288     return getTableInfoPath(tableName) != null;
289   }
290   
291   /**
292    * Find the most current table info file for the given table in the hbase root directory.
293    * @return The file status of the current table info file or null if it does not exist
294    */
295   private FileStatus getTableInfoPath(final TableName tableName) throws IOException {
296     Path tableDir = getTableDir(tableName);
297     return getTableInfoPath(tableDir);
298   }
299 
300   private FileStatus getTableInfoPath(Path tableDir)
301   throws IOException {
302     return getTableInfoPath(fs, tableDir, !fsreadonly);
303   }
304   
305   /**
306    * Find the most current table info file for the table located in the given table directory.
307    * 
308    * Looks within the {@link #TABLEINFO_DIR} subdirectory of the given directory for any table info
309    * files and takes the 'current' one - meaning the one with the highest sequence number if present
310    * or no sequence number at all if none exist (for backward compatibility from before there
311    * were sequence numbers).
312    * 
313    * @return The file status of the current table info file or null if it does not exist
314    * @throws IOException
315    */
316   public static FileStatus getTableInfoPath(FileSystem fs, Path tableDir)
317   throws IOException {
318     return getTableInfoPath(fs, tableDir, false);
319   }
320   
321   /**
322    * Find the most current table info file for the table in the given table directory.
323    * 
324    * Looks within the {@link #TABLEINFO_DIR} subdirectory of the given directory for any table info
325    * files and takes the 'current' one - meaning the one with the highest sequence number if
326    * present or no sequence number at all if none exist (for backward compatibility from before
327    * there were sequence numbers).
328    * If there are multiple table info files found and removeOldFiles is true it also deletes the
329    * older files.
330    * 
331    * @return The file status of the current table info file or null if none exist
332    * @throws IOException
333    */
334   private static FileStatus getTableInfoPath(FileSystem fs, Path tableDir, boolean removeOldFiles)
335   throws IOException {
336     Path tableInfoDir = new Path(tableDir, TABLEINFO_DIR);
337     return getCurrentTableInfoStatus(fs, tableInfoDir, removeOldFiles);
338   }
339   
340   /**
341    * Find the most current table info file in the given directory
342    * 
343    * Looks within the given directory for any table info files
344    * and takes the 'current' one - meaning the one with the highest sequence number if present
345    * or no sequence number at all if none exist (for backward compatibility from before there
346    * were sequence numbers).
347    * If there are multiple possible files found
348    * and the we're not in read only mode it also deletes the older files.
349    * 
350    * @return The file status of the current table info file or null if it does not exist
351    * @throws IOException
352    */
353   // only visible for FSTableDescriptorMigrationToSubdir, can be removed with that
354   static FileStatus getCurrentTableInfoStatus(FileSystem fs, Path dir, boolean removeOldFiles)
355   throws IOException {
356     FileStatus [] status = FSUtils.listStatus(fs, dir, TABLEINFO_PATHFILTER);
357     if (status == null || status.length < 1) return null;
358     FileStatus mostCurrent = null;
359     for (FileStatus file : status) {
360       if (mostCurrent == null || TABLEINFO_FILESTATUS_COMPARATOR.compare(file, mostCurrent) < 0) {
361         mostCurrent = file;
362       }
363     }
364     if (removeOldFiles && status.length > 1) {
365       // Clean away old versions
366       for (FileStatus file : status) {
367         Path path = file.getPath();
368         if (file != mostCurrent) {
369           if (!fs.delete(file.getPath(), false)) {
370             LOG.warn("Failed cleanup of " + path);
371           } else {
372             LOG.debug("Cleaned up old tableinfo file " + path);
373           }
374         }
375       }
376     }
377     return mostCurrent;
378   }
379   
380   /**
381    * Compare {@link FileStatus} instances by {@link Path#getName()}. Returns in
382    * reverse order.
383    */
384   @VisibleForTesting
385   static final Comparator<FileStatus> TABLEINFO_FILESTATUS_COMPARATOR =
386   new Comparator<FileStatus>() {
387     @Override
388     public int compare(FileStatus left, FileStatus right) {
389       return -left.compareTo(right);
390     }};
391 
392   /**
393    * Return the table directory in HDFS
394    */
395   @VisibleForTesting Path getTableDir(final TableName tableName) {
396     return FSUtils.getTableDir(rootdir, tableName);
397   }
398 
399   private static final PathFilter TABLEINFO_PATHFILTER = new PathFilter() {
400     @Override
401     public boolean accept(Path p) {
402       // Accept any file that starts with TABLEINFO_NAME
403       return p.getName().startsWith(TABLEINFO_FILE_PREFIX);
404     }}; 
405 
406   /**
407    * Width of the sequenceid that is a suffix on a tableinfo file.
408    */
409   @VisibleForTesting static final int WIDTH_OF_SEQUENCE_ID = 10;
410 
411   /*
412    * @param number Number to use as suffix.
413    * @return Returns zero-prefixed decimal version of passed
414    * number (Does absolute in case number is negative).
415    */
416   private static String formatTableInfoSequenceId(final int number) {
417     byte [] b = new byte[WIDTH_OF_SEQUENCE_ID];
418     int d = Math.abs(number);
419     for (int i = b.length - 1; i >= 0; i--) {
420       b[i] = (byte)((d % 10) + '0');
421       d /= 10;
422     }
423     return Bytes.toString(b);
424   }
425 
426   /**
427    * Regex to eat up sequenceid suffix on a .tableinfo file.
428    * Use regex because may encounter oldstyle .tableinfos where there is no
429    * sequenceid on the end.
430    */
431   private static final Pattern TABLEINFO_FILE_REGEX =
432     Pattern.compile(TABLEINFO_FILE_PREFIX + "(\\.([0-9]{" + WIDTH_OF_SEQUENCE_ID + "}))?$");
433 
434   /**
435    * @param p Path to a <code>.tableinfo</code> file.
436    * @return The current editid or 0 if none found.
437    */
438   @VisibleForTesting static int getTableInfoSequenceId(final Path p) {
439     if (p == null) return 0;
440     Matcher m = TABLEINFO_FILE_REGEX.matcher(p.getName());
441     if (!m.matches()) throw new IllegalArgumentException(p.toString());
442     String suffix = m.group(2);
443     if (suffix == null || suffix.length() <= 0) return 0;
444     return Integer.parseInt(m.group(2));
445   }
446 
447   /**
448    * @param tabledir
449    * @param sequenceid
450    * @return Name of tableinfo file.
451    */
452   @VisibleForTesting static String getTableInfoFileName(final int sequenceid) {
453     return TABLEINFO_FILE_PREFIX + "." + formatTableInfoSequenceId(sequenceid);
454   }
455 
456   /**
457    * @param fs
458    * @param rootdir
459    * @param tableName
460    * @return Modification time for the table {@link #TABLEINFO_FILE_PREFIX} file
461    * or <code>0</code> if no tableinfo file found.
462    * @throws IOException
463    */
464   private long getTableInfoModtime(final TableName tableName) throws IOException {
465     FileStatus status = getTableInfoPath(tableName);
466     return status == null ? 0 : status.getModificationTime();
467   }
468 
469   /**
470    * Returns the latest table descriptor for the given table directly from the file system
471    * if it exists, bypassing the local cache.
472    * Returns null if it's not found.
473    */
474   public static HTableDescriptor getTableDescriptorFromFs(FileSystem fs,
475       Path hbaseRootDir, TableName tableName) throws IOException {
476     Path tableDir = FSUtils.getTableDir(hbaseRootDir, tableName);
477     return getTableDescriptorFromFs(fs, tableDir);
478   }
479 
480   /**
481    * Returns the latest table descriptor for the table located at the given directory
482    * directly from the file system if it exists.
483    * @throws TableInfoMissingException if there is no descriptor
484    */
485   public static HTableDescriptor getTableDescriptorFromFs(FileSystem fs, Path tableDir)
486   throws IOException {
487     FileStatus status = getTableInfoPath(fs, tableDir, false);
488     if (status == null) {
489       throw new TableInfoMissingException("No table descriptor file under " + tableDir);
490     }
491     return readTableDescriptor(fs, status, false);
492   }
493   
494   private TableDescriptorAndModtime getTableDescriptorAndModtime(TableName tableName)
495   throws IOException {
496     // ignore both -ROOT- and .META. tables
497     if (tableName.equals(TableName.ROOT_TABLE_NAME)
498         || tableName.equals(TableName.META_TABLE_NAME)) {
499       return null;
500     }
501     return getTableDescriptorAndModtime(getTableDir(tableName));
502   }
503 
504   private TableDescriptorAndModtime getTableDescriptorAndModtime(Path tableDir)
505   throws IOException {
506     FileStatus status = getTableInfoPath(tableDir);
507     if (status == null) {
508       throw new TableInfoMissingException("No table descriptor file under " + tableDir);
509     }
510     HTableDescriptor htd = readTableDescriptor(fs, status, !fsreadonly);
511     return new TableDescriptorAndModtime(status.getModificationTime(), htd);
512   }
513 
514   private static HTableDescriptor readTableDescriptor(FileSystem fs, FileStatus status,
515       boolean rewritePb) throws IOException {
516     int len = Ints.checkedCast(status.getLen());
517     byte [] content = new byte[len];
518     FSDataInputStream fsDataInputStream = fs.open(status.getPath());
519     try {
520       fsDataInputStream.readFully(content);
521     } finally {
522       fsDataInputStream.close();
523     }
524     HTableDescriptor htd = null;
525     try {
526       htd = HTableDescriptor.parseFrom(content);
527     } catch (DeserializationException e) {
528       throw new IOException("content=" + Bytes.toShort(content), e);
529     }
530     if (rewritePb && !ProtobufUtil.isPBMagicPrefix(content)) {
531       // Convert the file over to be pb before leaving here.
532       Path tableInfoDir = status.getPath().getParent();
533       Path tableDir = tableInfoDir.getParent();
534       writeTableDescriptor(fs, htd, tableDir, status);
535     }
536     return htd;
537   }
538  
539   /**
540    * Update table descriptor on the file system
541    * @throws IOException Thrown if failed update.
542    * @throws NotImplementedException if in read only mode
543    */
544   @VisibleForTesting Path updateTableDescriptor(HTableDescriptor htd)
545   throws IOException {
546     if (fsreadonly) {
547       throw new NotImplementedException("Cannot update a table descriptor - in read only mode");
548     }
549     Path tableDir = getTableDir(htd.getTableName());
550     Path p = writeTableDescriptor(fs, htd, tableDir, getTableInfoPath(tableDir));
551     if (p == null) throw new IOException("Failed update");
552     LOG.info("Updated tableinfo=" + p);
553     return p;
554   }
555 
556   /**
557    * Deletes all the table descriptor files from the file system.
558    * Used in unit tests only.
559    * @throws NotImplementedException if in read only mode
560    */
561   public void deleteTableDescriptorIfExists(TableName tableName) throws IOException {
562     if (fsreadonly) {
563       throw new NotImplementedException("Cannot delete a table descriptor - in read only mode");
564     }
565    
566     Path tableDir = getTableDir(tableName);
567     Path tableInfoDir = new Path(tableDir, TABLEINFO_DIR);
568     deleteTableDescriptorFiles(fs, tableInfoDir, Integer.MAX_VALUE);
569   }
570 
571   /**
572    * Deletes files matching the table info file pattern within the given directory 
573    * whose sequenceId is at most the given max sequenceId.
574    */
575   private static void deleteTableDescriptorFiles(FileSystem fs, Path dir, int maxSequenceId)
576   throws IOException {
577     FileStatus [] status = FSUtils.listStatus(fs, dir, TABLEINFO_PATHFILTER);
578     for (FileStatus file : status) {
579       Path path = file.getPath();
580       int sequenceId = getTableInfoSequenceId(path);
581       if (sequenceId <= maxSequenceId) {
582         boolean success = FSUtils.delete(fs, path, false);
583         if (success) {
584           LOG.debug("Deleted table descriptor at " + path);
585         } else {
586           LOG.error("Failed to delete descriptor at " + path);
587         }
588       }
589     }
590   }
591   
592   /**
593    * Attempts to write a new table descriptor to the given table's directory.
594    * It first writes it to the .tmp dir then uses an atomic rename to move it into place.
595    * It begins at the currentSequenceId + 1 and tries 10 times to find a new sequence number
596    * not already in use.
597    * Removes the current descriptor file if passed in.
598    * 
599    * @return Descriptor file or null if we failed write.
600    */
601   private static Path writeTableDescriptor(final FileSystem fs, 
602     final HTableDescriptor htd, final Path tableDir,
603     final FileStatus currentDescriptorFile)
604   throws IOException {  
605     // Get temporary dir into which we'll first write a file to avoid half-written file phenomenon.
606     // This directory is never removed to avoid removing it out from under a concurrent writer.
607     Path tmpTableDir = new Path(tableDir, TMP_DIR);
608     Path tableInfoDir = new Path(tableDir, TABLEINFO_DIR);
609     
610     // What is current sequenceid?  We read the current sequenceid from
611     // the current file.  After we read it, another thread could come in and
612     // compete with us writing out next version of file.  The below retries
613     // should help in this case some but its hard to do guarantees in face of
614     // concurrent schema edits.
615     int currentSequenceId = currentDescriptorFile == null ? 0 :
616       getTableInfoSequenceId(currentDescriptorFile.getPath());
617     int newSequenceId = currentSequenceId;
618     
619     // Put arbitrary upperbound on how often we retry
620     int retries = 10;
621     int retrymax = currentSequenceId + retries;
622     Path tableInfoDirPath = null;
623     do {
624       newSequenceId += 1;
625       String filename = getTableInfoFileName(newSequenceId);
626       Path tempPath = new Path(tmpTableDir, filename);
627       if (fs.exists(tempPath)) {
628         LOG.debug(tempPath + " exists; retrying up to " + retries + " times");
629         continue;
630       }
631       tableInfoDirPath = new Path(tableInfoDir, filename);
632       try {
633         writeHTD(fs, tempPath, htd);
634         fs.mkdirs(tableInfoDirPath.getParent());
635         if (!fs.rename(tempPath, tableInfoDirPath)) {
636           throw new IOException("Failed rename of " + tempPath + " to " + tableInfoDirPath);
637         }
638         LOG.debug("Wrote descriptor into: " + tableInfoDirPath);
639       } catch (IOException ioe) {
640         // Presume clash of names or something; go around again.
641         LOG.debug("Failed write and/or rename; retrying", ioe);
642         if (!FSUtils.deleteDirectory(fs, tempPath)) {
643           LOG.warn("Failed cleanup of " + tempPath);
644         }
645         tableInfoDirPath = null;
646         continue;
647       }
648       break;
649     } while (newSequenceId < retrymax);
650     if (tableInfoDirPath != null) {
651       // if we succeeded, remove old table info files.
652       deleteTableDescriptorFiles(fs, tableInfoDir, newSequenceId - 1);
653     }
654     return tableInfoDirPath;
655   }
656   
657   private static void writeHTD(final FileSystem fs, final Path p, final HTableDescriptor htd)
658   throws IOException {
659     FSDataOutputStream out = fs.create(p, false);
660     try {
661       // We used to write this file out as a serialized HTD Writable followed by two '\n's and then
662       // the toString version of HTD.  Now we just write out the pb serialization.
663       out.write(htd.toByteArray());
664     } finally {
665       out.close();
666     }
667   }
668 
669   /**
670    * Create new HTableDescriptor in HDFS. Happens when we are creating table.
671    * Used by tests.
672    * @return True if we successfully created file.
673    */
674   public boolean createTableDescriptor(HTableDescriptor htd) throws IOException {
675     return createTableDescriptor(htd, false);
676   }
677 
678   /**
679    * Create new HTableDescriptor in HDFS. Happens when we are creating table. If
680    * forceCreation is true then even if previous table descriptor is present it
681    * will be overwritten
682    * 
683    * @return True if we successfully created file.
684    */
685   public boolean createTableDescriptor(HTableDescriptor htd, boolean forceCreation)
686   throws IOException {
687     Path tableDir = getTableDir(htd.getTableName());
688     return createTableDescriptorForTableDirectory(tableDir, htd, forceCreation);
689   }
690   
691   /**
692    * Create a new HTableDescriptor in HDFS in the specified table directory. Happens when we create
693    * a new table or snapshot a table.
694    * @param tableDir table directory under which we should write the file
695    * @param htd description of the table to write
696    * @param forceCreation if <tt>true</tt>,then even if previous table descriptor is present it will
697    *          be overwritten
698    * @return <tt>true</tt> if the we successfully created the file, <tt>false</tt> if the file
699    *         already exists and we weren't forcing the descriptor creation.
700    * @throws IOException if a filesystem error occurs
701    */
702   public boolean createTableDescriptorForTableDirectory(Path tableDir,
703       HTableDescriptor htd, boolean forceCreation) throws IOException {
704     if (fsreadonly) {
705       throw new NotImplementedException("Cannot create a table descriptor - in read only mode");
706     }
707     FileStatus status = getTableInfoPath(fs, tableDir);
708     if (status != null) {
709       LOG.debug("Current tableInfoPath = " + status.getPath());
710       if (!forceCreation) {
711         if (fs.exists(status.getPath()) && status.getLen() > 0) {
712           if (readTableDescriptor(fs, status, false).equals(htd)) {
713             LOG.debug("TableInfo already exists.. Skipping creation");
714             return false;
715           }
716         }
717       }
718     }
719     Path p = writeTableDescriptor(fs, htd, tableDir, status);
720     return p != null;
721   }
722   
723 }
724