recovery.c revision 289180
1/* recovery.c --- FSFS recovery functionality
2*
3 * ====================================================================
4 *    Licensed to the Apache Software Foundation (ASF) under one
5 *    or more contributor license agreements.  See the NOTICE file
6 *    distributed with this work for additional information
7 *    regarding copyright ownership.  The ASF licenses this file
8 *    to you under the Apache License, Version 2.0 (the
9 *    "License"); you may not use this file except in compliance
10 *    with the License.  You may obtain a copy of the License at
11 *
12 *      http://www.apache.org/licenses/LICENSE-2.0
13 *
14 *    Unless required by applicable law or agreed to in writing,
15 *    software distributed under the License is distributed on an
16 *    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17 *    KIND, either express or implied.  See the License for the
18 *    specific language governing permissions and limitations
19 *    under the License.
20 * ====================================================================
21 */
22
23#include "recovery.h"
24
25#include "svn_hash.h"
26#include "svn_pools.h"
27#include "private/svn_string_private.h"
28
29#include "index.h"
30#include "low_level.h"
31#include "rep-cache.h"
32#include "revprops.h"
33#include "util.h"
34#include "cached_data.h"
35
36#include "../libsvn_fs/fs-loader.h"
37
38#include "svn_private_config.h"
39
40/* Part of the recovery procedure.  Return the largest revision *REV in
41   filesystem FS.  Use POOL for temporary allocation. */
42static svn_error_t *
43recover_get_largest_revision(svn_fs_t *fs, svn_revnum_t *rev, apr_pool_t *pool)
44{
45  /* Discovering the largest revision in the filesystem would be an
46     expensive operation if we did a readdir() or searched linearly,
47     so we'll do a form of binary search.  left is a revision that we
48     know exists, right a revision that we know does not exist. */
49  apr_pool_t *iterpool;
50  svn_revnum_t left, right = 1;
51
52  iterpool = svn_pool_create(pool);
53  /* Keep doubling right, until we find a revision that doesn't exist. */
54  while (1)
55    {
56      svn_error_t *err;
57      svn_fs_fs__revision_file_t *file;
58      svn_pool_clear(iterpool);
59
60      err = svn_fs_fs__open_pack_or_rev_file(&file, fs, right, iterpool,
61                                             iterpool);
62      if (err && err->apr_err == SVN_ERR_FS_NO_SUCH_REVISION)
63        {
64          svn_error_clear(err);
65          break;
66        }
67      else
68        SVN_ERR(err);
69
70      right <<= 1;
71    }
72
73  left = right >> 1;
74
75  /* We know that left exists and right doesn't.  Do a normal bsearch to find
76     the last revision. */
77  while (left + 1 < right)
78    {
79      svn_revnum_t probe = left + ((right - left) / 2);
80      svn_error_t *err;
81      svn_fs_fs__revision_file_t *file;
82      svn_pool_clear(iterpool);
83
84      err = svn_fs_fs__open_pack_or_rev_file(&file, fs, probe, iterpool,
85                                             iterpool);
86      if (err && err->apr_err == SVN_ERR_FS_NO_SUCH_REVISION)
87        {
88          svn_error_clear(err);
89          right = probe;
90        }
91      else
92        {
93          SVN_ERR(err);
94          left = probe;
95        }
96    }
97
98  svn_pool_destroy(iterpool);
99
100  /* left is now the largest revision that exists. */
101  *rev = left;
102  return SVN_NO_ERROR;
103}
104
105/* A baton for reading a fixed amount from an open file.  For
106   recover_find_max_ids() below. */
107struct recover_read_from_file_baton
108{
109  svn_stream_t *stream;
110  apr_pool_t *pool;
111  apr_off_t remaining;
112};
113
114/* A stream read handler used by recover_find_max_ids() below.
115   Read and return at most BATON->REMAINING bytes from the stream,
116   returning nothing after that to indicate EOF. */
117static svn_error_t *
118read_handler_recover(void *baton, char *buffer, apr_size_t *len)
119{
120  struct recover_read_from_file_baton *b = baton;
121  apr_size_t bytes_to_read = *len;
122
123  if (b->remaining == 0)
124    {
125      /* Return a successful read of zero bytes to signal EOF. */
126      *len = 0;
127      return SVN_NO_ERROR;
128    }
129
130  if ((apr_int64_t)bytes_to_read > (apr_int64_t)b->remaining)
131    bytes_to_read = (apr_size_t)b->remaining;
132  b->remaining -= bytes_to_read;
133
134  return svn_stream_read_full(b->stream, buffer, &bytes_to_read);
135}
136
137/* Part of the recovery procedure.  Read the directory noderev at offset
138   OFFSET of file REV_FILE (the revision file of revision REV of
139   filesystem FS), and set MAX_NODE_ID and MAX_COPY_ID to be the node-id
140   and copy-id of that node, if greater than the current value stored
141   in either.  Recurse into any child directories that were modified in
142   this revision.
143
144   MAX_NODE_ID and MAX_COPY_ID must be arrays of at least MAX_KEY_SIZE.
145
146   Perform temporary allocation in POOL. */
147static svn_error_t *
148recover_find_max_ids(svn_fs_t *fs,
149                     svn_revnum_t rev,
150                     svn_fs_fs__revision_file_t *rev_file,
151                     apr_off_t offset,
152                     apr_uint64_t *max_node_id,
153                     apr_uint64_t *max_copy_id,
154                     apr_pool_t *pool)
155{
156  svn_fs_fs__rep_header_t *header;
157  struct recover_read_from_file_baton baton;
158  svn_stream_t *stream;
159  apr_hash_t *entries;
160  apr_hash_index_t *hi;
161  apr_pool_t *iterpool;
162  node_revision_t *noderev;
163  svn_error_t *err;
164
165  baton.stream = rev_file->stream;
166  SVN_ERR(svn_io_file_seek(rev_file->file, APR_SET, &offset, pool));
167  SVN_ERR(svn_fs_fs__read_noderev(&noderev, baton.stream, pool, pool));
168
169  /* Check that this is a directory.  It should be. */
170  if (noderev->kind != svn_node_dir)
171    return svn_error_create(SVN_ERR_FS_CORRUPT, NULL,
172                            _("Recovery encountered a non-directory node"));
173
174  /* Get the data location.  No data location indicates an empty directory. */
175  if (!noderev->data_rep)
176    return SVN_NO_ERROR;
177
178  /* If the directory's data representation wasn't changed in this revision,
179     we've already scanned the directory's contents for noderevs, so we don't
180     need to again.  This will occur if a property is changed on a directory
181     without changing the directory's contents. */
182  if (noderev->data_rep->revision != rev)
183    return SVN_NO_ERROR;
184
185  /* We could use get_dir_contents(), but this is much cheaper.  It does
186     rely on directory entries being stored as PLAIN reps, though. */
187  SVN_ERR(svn_fs_fs__item_offset(&offset, fs, rev_file, rev, NULL,
188                                 noderev->data_rep->item_index, pool));
189  SVN_ERR(svn_io_file_seek(rev_file->file, APR_SET, &offset, pool));
190  SVN_ERR(svn_fs_fs__read_rep_header(&header, baton.stream, pool, pool));
191  if (header->type != svn_fs_fs__rep_plain)
192    return svn_error_create(SVN_ERR_FS_CORRUPT, NULL,
193                            _("Recovery encountered a deltified directory "
194                              "representation"));
195
196  /* Now create a stream that's allowed to read only as much data as is
197     stored in the representation.  Note that this is a directory, i.e.
198     represented using the hash format on disk and can never have 0 length. */
199  baton.pool = pool;
200  baton.remaining = noderev->data_rep->expanded_size
201                  ? noderev->data_rep->expanded_size
202                  : noderev->data_rep->size;
203  stream = svn_stream_create(&baton, pool);
204  svn_stream_set_read2(stream, NULL /* only full read support */,
205                       read_handler_recover);
206
207  /* Now read the entries from that stream. */
208  entries = apr_hash_make(pool);
209  err = svn_hash_read2(entries, stream, SVN_HASH_TERMINATOR, pool);
210  if (err)
211    {
212      svn_string_t *id_str = svn_fs_fs__id_unparse(noderev->id, pool);
213
214      svn_error_clear(svn_stream_close(stream));
215      return svn_error_quick_wrapf(err,
216                _("malformed representation for node-revision '%s'"),
217                id_str->data);
218    }
219  SVN_ERR(svn_stream_close(stream));
220
221  /* Now check each of the entries in our directory to find new node and
222     copy ids, and recurse into new subdirectories. */
223  iterpool = svn_pool_create(pool);
224  for (hi = apr_hash_first(pool, entries); hi; hi = apr_hash_next(hi))
225    {
226      char *str_val;
227      char *str;
228      svn_node_kind_t kind;
229      const svn_fs_id_t *id;
230      const svn_fs_fs__id_part_t *rev_item;
231      apr_uint64_t node_id, copy_id;
232      apr_off_t child_dir_offset;
233      const svn_string_t *path = apr_hash_this_val(hi);
234
235      svn_pool_clear(iterpool);
236
237      str_val = apr_pstrdup(iterpool, path->data);
238
239      str = svn_cstring_tokenize(" ", &str_val);
240      if (str == NULL)
241        return svn_error_create(SVN_ERR_FS_CORRUPT, NULL,
242                                _("Directory entry corrupt"));
243
244      if (strcmp(str, SVN_FS_FS__KIND_FILE) == 0)
245        kind = svn_node_file;
246      else if (strcmp(str, SVN_FS_FS__KIND_DIR) == 0)
247        kind = svn_node_dir;
248      else
249        {
250          return svn_error_create(SVN_ERR_FS_CORRUPT, NULL,
251                                  _("Directory entry corrupt"));
252        }
253
254      str = svn_cstring_tokenize(" ", &str_val);
255      if (str == NULL)
256        return svn_error_create(SVN_ERR_FS_CORRUPT, NULL,
257                                _("Directory entry corrupt"));
258
259      SVN_ERR(svn_fs_fs__id_parse(&id, str, iterpool));
260
261      rev_item = svn_fs_fs__id_rev_item(id);
262      if (rev_item->revision != rev)
263        {
264          /* If the node wasn't modified in this revision, we've already
265             checked the node and copy id. */
266          continue;
267        }
268
269      node_id = svn_fs_fs__id_node_id(id)->number;
270      copy_id = svn_fs_fs__id_copy_id(id)->number;
271
272      if (node_id > *max_node_id)
273        *max_node_id = node_id;
274      if (copy_id > *max_copy_id)
275        *max_copy_id = copy_id;
276
277      if (kind == svn_node_file)
278        continue;
279
280      SVN_ERR(svn_fs_fs__item_offset(&child_dir_offset, fs,
281                                     rev_file, rev, NULL, rev_item->number,
282                                     iterpool));
283      SVN_ERR(recover_find_max_ids(fs, rev, rev_file, child_dir_offset,
284                                   max_node_id, max_copy_id, iterpool));
285    }
286  svn_pool_destroy(iterpool);
287
288  return SVN_NO_ERROR;
289}
290
291/* Part of the recovery procedure.  Given an open non-packed revision file
292   REV_FILE for REV, locate the trailer that specifies the offset to the root
293   node-id and store this offset in *ROOT_OFFSET.  Do temporary allocations in
294   POOL. */
295static svn_error_t *
296recover_get_root_offset(apr_off_t *root_offset,
297                        svn_revnum_t rev,
298                        svn_fs_fs__revision_file_t *rev_file,
299                        apr_pool_t *pool)
300{
301  char buffer[64];
302  svn_stringbuf_t *trailer;
303  apr_off_t start;
304  apr_off_t end;
305  apr_size_t len;
306
307  SVN_ERR_ASSERT(!rev_file->is_packed);
308
309  /* We will assume that the last line containing the two offsets (to the root
310     node-id and to the changed path information) will never be longer than 64
311     characters. */
312  end = 0;
313  SVN_ERR(svn_io_file_seek(rev_file->file, APR_END, &end, pool));
314
315  if (end < sizeof(buffer))
316    {
317      len = (apr_size_t)end;
318      start = 0;
319    }
320  else
321    {
322      len = sizeof(buffer);
323      start = end - sizeof(buffer);
324    }
325
326  SVN_ERR(svn_io_file_seek(rev_file->file, APR_SET, &start, pool));
327  SVN_ERR(svn_io_file_read_full2(rev_file->file, buffer, len,
328                                 NULL, NULL, pool));
329
330  trailer = svn_stringbuf_ncreate(buffer, len, pool);
331  SVN_ERR(svn_fs_fs__parse_revision_trailer(root_offset, NULL, trailer, rev));
332
333  return SVN_NO_ERROR;
334}
335
336/* Baton used for recover_body below. */
337struct recover_baton {
338  svn_fs_t *fs;
339  svn_cancel_func_t cancel_func;
340  void *cancel_baton;
341};
342
343/* The work-horse for svn_fs_fs__recover, called with the FS
344   write lock.  This implements the svn_fs_fs__with_write_lock()
345   'body' callback type.  BATON is a 'struct recover_baton *'. */
346static svn_error_t *
347recover_body(void *baton, apr_pool_t *pool)
348{
349  struct recover_baton *b = baton;
350  svn_fs_t *fs = b->fs;
351  fs_fs_data_t *ffd = fs->fsap_data;
352  svn_revnum_t max_rev;
353  apr_uint64_t next_node_id = 0;
354  apr_uint64_t next_copy_id = 0;
355  svn_revnum_t youngest_rev;
356  svn_node_kind_t youngest_revprops_kind;
357
358  /* The admin may have created a plain copy of this repo before attempting
359     to recover it (hotcopy may or may not work with corrupted repos).
360     Bump the instance ID. */
361  SVN_ERR(svn_fs_fs__set_uuid(fs, fs->uuid, NULL, pool));
362
363  /* We need to know the largest revision in the filesystem. */
364  SVN_ERR(recover_get_largest_revision(fs, &max_rev, pool));
365
366  /* Get the expected youngest revision */
367  SVN_ERR(svn_fs_fs__youngest_rev(&youngest_rev, fs, pool));
368
369  /* Policy note:
370
371     Since the revprops file is written after the revs file, the true
372     maximum available revision is the youngest one for which both are
373     present.  That's probably the same as the max_rev we just found,
374     but if it's not, we could, in theory, repeatedly decrement
375     max_rev until we find a revision that has both a revs and
376     revprops file, then write db/current with that.
377
378     But we choose not to.  If a repository is so corrupt that it's
379     missing at least one revprops file, we shouldn't assume that the
380     youngest revision for which both the revs and revprops files are
381     present is healthy.  In other words, we're willing to recover
382     from a missing or out-of-date db/current file, because db/current
383     is truly redundant -- it's basically a cache so we don't have to
384     find max_rev each time, albeit a cache with unusual semantics,
385     since it also officially defines when a revision goes live.  But
386     if we're missing more than the cache, it's time to back out and
387     let the admin reconstruct things by hand: correctness at that
388     point may depend on external things like checking a commit email
389     list, looking in particular working copies, etc.
390
391     This policy matches well with a typical naive backup scenario.
392     Say you're rsyncing your FSFS repository nightly to the same
393     location.  Once revs and revprops are written, you've got the
394     maximum rev; if the backup should bomb before db/current is
395     written, then db/current could stay arbitrarily out-of-date, but
396     we can still recover.  It's a small window, but we might as well
397     do what we can. */
398
399  /* Even if db/current were missing, it would be created with 0 by
400     get_youngest(), so this conditional remains valid. */
401  if (youngest_rev > max_rev)
402    return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL,
403                             _("Expected current rev to be <= %ld "
404                               "but found %ld"), max_rev, youngest_rev);
405
406  /* We only need to search for maximum IDs for old FS formats which
407     se global ID counters. */
408  if (ffd->format < SVN_FS_FS__MIN_NO_GLOBAL_IDS_FORMAT)
409    {
410      /* Next we need to find the maximum node id and copy id in use across the
411         filesystem.  Unfortunately, the only way we can get this information
412         is to scan all the noderevs of all the revisions and keep track as
413         we go along. */
414      svn_revnum_t rev;
415      apr_pool_t *iterpool = svn_pool_create(pool);
416
417      for (rev = 0; rev <= max_rev; rev++)
418        {
419          svn_fs_fs__revision_file_t *rev_file;
420          apr_off_t root_offset;
421
422          svn_pool_clear(iterpool);
423
424          if (b->cancel_func)
425            SVN_ERR(b->cancel_func(b->cancel_baton));
426
427          SVN_ERR(svn_fs_fs__open_pack_or_rev_file(&rev_file, fs, rev, pool,
428                                                   iterpool));
429          SVN_ERR(recover_get_root_offset(&root_offset, rev, rev_file, pool));
430          SVN_ERR(recover_find_max_ids(fs, rev, rev_file, root_offset,
431                                       &next_node_id, &next_copy_id, pool));
432          SVN_ERR(svn_fs_fs__close_revision_file(rev_file));
433        }
434      svn_pool_destroy(iterpool);
435
436      /* Now that we finally have the maximum revision, node-id and copy-id, we
437         can bump the two ids to get the next of each. */
438      next_node_id++;
439      next_copy_id++;
440    }
441
442  /* Before setting current, verify that there is a revprops file
443     for the youngest revision.  (Issue #2992) */
444  SVN_ERR(svn_io_check_path(svn_fs_fs__path_revprops(fs, max_rev, pool),
445                            &youngest_revprops_kind, pool));
446  if (youngest_revprops_kind == svn_node_none)
447    {
448      svn_boolean_t missing = TRUE;
449      if (!svn_fs_fs__packed_revprop_available(&missing, fs, max_rev, pool))
450        {
451          if (missing)
452            {
453              return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL,
454                                      _("Revision %ld has a revs file but no "
455                                        "revprops file"),
456                                      max_rev);
457            }
458          else
459            {
460              return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL,
461                                      _("Revision %ld has a revs file but the "
462                                        "revprops file is inaccessible"),
463                                      max_rev);
464            }
465          }
466    }
467  else if (youngest_revprops_kind != svn_node_file)
468    {
469      return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL,
470                               _("Revision %ld has a non-file where its "
471                                 "revprops file should be"),
472                               max_rev);
473    }
474
475  /* Prune younger-than-(newfound-youngest) revisions from the rep
476     cache if sharing is enabled taking care not to create the cache
477     if it does not exist. */
478  if (ffd->rep_sharing_allowed)
479    {
480      svn_boolean_t rep_cache_exists;
481
482      SVN_ERR(svn_fs_fs__exists_rep_cache(&rep_cache_exists, fs, pool));
483      if (rep_cache_exists)
484        SVN_ERR(svn_fs_fs__del_rep_reference(fs, max_rev, pool));
485    }
486
487  /* Now store the discovered youngest revision, and the next IDs if
488     relevant, in a new 'current' file. */
489  return svn_fs_fs__write_current(fs, max_rev, next_node_id, next_copy_id,
490                                  pool);
491}
492
493/* This implements the fs_library_vtable_t.recover() API. */
494svn_error_t *
495svn_fs_fs__recover(svn_fs_t *fs,
496                   svn_cancel_func_t cancel_func, void *cancel_baton,
497                   apr_pool_t *pool)
498{
499  struct recover_baton b;
500
501  /* We have no way to take out an exclusive lock in FSFS, so we're
502     restricted as to the types of recovery we can do.  Luckily,
503     we just want to recreate the 'current' file, and we can do that just
504     by blocking other writers. */
505  b.fs = fs;
506  b.cancel_func = cancel_func;
507  b.cancel_baton = cancel_baton;
508  return svn_fs_fs__with_all_locks(fs, recover_body, &b, pool);
509}
510