github.com/skoak/go-ethereum@v1.9.7/core/rawdb/freezer_table_test.go (about)

     1  // Copyright 2019 The go-ethereum Authors
     2  // This file is part of the go-ethereum library.
     3  //
     4  // The go-ethereum library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The go-ethereum library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package rawdb
    18  
    19  import (
    20  	"bytes"
    21  	"fmt"
    22  	"math/rand"
    23  	"os"
    24  	"path/filepath"
    25  	"testing"
    26  	"time"
    27  
    28  	"github.com/ethereum/go-ethereum/metrics"
    29  )
    30  
    31  func init() {
    32  	rand.Seed(time.Now().Unix())
    33  }
    34  
    35  // Gets a chunk of data, filled with 'b'
    36  func getChunk(size int, b int) []byte {
    37  	data := make([]byte, size)
    38  	for i := range data {
    39  		data[i] = byte(b)
    40  	}
    41  	return data
    42  }
    43  
    44  func print(t *testing.T, f *freezerTable, item uint64) {
    45  	a, err := f.Retrieve(item)
    46  	if err != nil {
    47  		t.Fatal(err)
    48  	}
    49  	t.Logf("db[%d] =  %x\n", item, a)
    50  }
    51  
    52  // TestFreezerBasics test initializing a freezertable from scratch, writing to the table,
    53  // and reading it back.
    54  func TestFreezerBasics(t *testing.T) {
    55  	t.Parallel()
    56  	// set cutoff at 50 bytes
    57  	f, err := newCustomTable(os.TempDir(),
    58  		fmt.Sprintf("unittest-%d", rand.Uint64()),
    59  		metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge(), 50, true)
    60  	if err != nil {
    61  		t.Fatal(err)
    62  	}
    63  	defer f.Close()
    64  	// Write 15 bytes 255 times, results in 85 files
    65  	for x := 0; x < 255; x++ {
    66  		data := getChunk(15, x)
    67  		f.Append(uint64(x), data)
    68  	}
    69  
    70  	//print(t, f, 0)
    71  	//print(t, f, 1)
    72  	//print(t, f, 2)
    73  	//
    74  	//db[0] =  000000000000000000000000000000
    75  	//db[1] =  010101010101010101010101010101
    76  	//db[2] =  020202020202020202020202020202
    77  
    78  	for y := 0; y < 255; y++ {
    79  		exp := getChunk(15, y)
    80  		got, err := f.Retrieve(uint64(y))
    81  		if err != nil {
    82  			t.Fatal(err)
    83  		}
    84  		if !bytes.Equal(got, exp) {
    85  			t.Fatalf("test %d, got \n%x != \n%x", y, got, exp)
    86  		}
    87  	}
    88  	// Check that we cannot read too far
    89  	_, err = f.Retrieve(uint64(255))
    90  	if err != errOutOfBounds {
    91  		t.Fatal(err)
    92  	}
    93  }
    94  
    95  // TestFreezerBasicsClosing tests same as TestFreezerBasics, but also closes and reopens the freezer between
    96  // every operation
    97  func TestFreezerBasicsClosing(t *testing.T) {
    98  	t.Parallel()
    99  	// set cutoff at 50 bytes
   100  	var (
   101  		fname      = fmt.Sprintf("basics-close-%d", rand.Uint64())
   102  		rm, wm, sg = metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge()
   103  		f          *freezerTable
   104  		err        error
   105  	)
   106  	f, err = newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
   107  	if err != nil {
   108  		t.Fatal(err)
   109  	}
   110  	// Write 15 bytes 255 times, results in 85 files
   111  	for x := 0; x < 255; x++ {
   112  		data := getChunk(15, x)
   113  		f.Append(uint64(x), data)
   114  		f.Close()
   115  		f, err = newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
   116  		if err != nil {
   117  			t.Fatal(err)
   118  		}
   119  	}
   120  	defer f.Close()
   121  
   122  	for y := 0; y < 255; y++ {
   123  		exp := getChunk(15, y)
   124  		got, err := f.Retrieve(uint64(y))
   125  		if err != nil {
   126  			t.Fatal(err)
   127  		}
   128  		if !bytes.Equal(got, exp) {
   129  			t.Fatalf("test %d, got \n%x != \n%x", y, got, exp)
   130  		}
   131  		f.Close()
   132  		f, err = newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
   133  		if err != nil {
   134  			t.Fatal(err)
   135  		}
   136  	}
   137  }
   138  
   139  // TestFreezerRepairDanglingHead tests that we can recover if index entries are removed
   140  func TestFreezerRepairDanglingHead(t *testing.T) {
   141  	t.Parallel()
   142  	rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge()
   143  	fname := fmt.Sprintf("dangling_headtest-%d", rand.Uint64())
   144  
   145  	{ // Fill table
   146  		f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
   147  		if err != nil {
   148  			t.Fatal(err)
   149  		}
   150  		// Write 15 bytes 255 times
   151  		for x := 0; x < 255; x++ {
   152  			data := getChunk(15, x)
   153  			f.Append(uint64(x), data)
   154  		}
   155  		// The last item should be there
   156  		if _, err = f.Retrieve(0xfe); err != nil {
   157  			t.Fatal(err)
   158  		}
   159  		f.Close()
   160  	}
   161  	// open the index
   162  	idxFile, err := os.OpenFile(filepath.Join(os.TempDir(), fmt.Sprintf("%s.ridx", fname)), os.O_RDWR, 0644)
   163  	if err != nil {
   164  		t.Fatalf("Failed to open index file: %v", err)
   165  	}
   166  	// Remove 4 bytes
   167  	stat, err := idxFile.Stat()
   168  	if err != nil {
   169  		t.Fatalf("Failed to stat index file: %v", err)
   170  	}
   171  	idxFile.Truncate(stat.Size() - 4)
   172  	idxFile.Close()
   173  	// Now open it again
   174  	{
   175  		f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
   176  		if err != nil {
   177  			t.Fatal(err)
   178  		}
   179  		// The last item should be missing
   180  		if _, err = f.Retrieve(0xff); err == nil {
   181  			t.Errorf("Expected error for missing index entry")
   182  		}
   183  		// The one before should still be there
   184  		if _, err = f.Retrieve(0xfd); err != nil {
   185  			t.Fatalf("Expected no error, got %v", err)
   186  		}
   187  	}
   188  }
   189  
   190  // TestFreezerRepairDanglingHeadLarge tests that we can recover if very many index entries are removed
   191  func TestFreezerRepairDanglingHeadLarge(t *testing.T) {
   192  	t.Parallel()
   193  	rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge()
   194  	fname := fmt.Sprintf("dangling_headtest-%d", rand.Uint64())
   195  
   196  	{ // Fill a table and close it
   197  		f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
   198  		if err != nil {
   199  			t.Fatal(err)
   200  		}
   201  		// Write 15 bytes 255 times
   202  		for x := 0; x < 0xff; x++ {
   203  			data := getChunk(15, x)
   204  			f.Append(uint64(x), data)
   205  		}
   206  		// The last item should be there
   207  		if _, err = f.Retrieve(f.items - 1); err == nil {
   208  			if err != nil {
   209  				t.Fatal(err)
   210  			}
   211  		}
   212  		f.Close()
   213  	}
   214  	// open the index
   215  	idxFile, err := os.OpenFile(filepath.Join(os.TempDir(), fmt.Sprintf("%s.ridx", fname)), os.O_RDWR, 0644)
   216  	if err != nil {
   217  		t.Fatalf("Failed to open index file: %v", err)
   218  	}
   219  	// Remove everything but the first item, and leave data unaligned
   220  	// 0-indexEntry, 1-indexEntry, corrupt-indexEntry
   221  	idxFile.Truncate(indexEntrySize + indexEntrySize + indexEntrySize/2)
   222  	idxFile.Close()
   223  	// Now open it again
   224  	{
   225  		f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
   226  		if err != nil {
   227  			t.Fatal(err)
   228  		}
   229  		// The first item should be there
   230  		if _, err = f.Retrieve(0); err != nil {
   231  			t.Fatal(err)
   232  		}
   233  		// The second item should be missing
   234  		if _, err = f.Retrieve(1); err == nil {
   235  			t.Errorf("Expected error for missing index entry")
   236  		}
   237  		// We should now be able to store items again, from item = 1
   238  		for x := 1; x < 0xff; x++ {
   239  			data := getChunk(15, ^x)
   240  			f.Append(uint64(x), data)
   241  		}
   242  		f.Close()
   243  	}
   244  	// And if we open it, we should now be able to read all of them (new values)
   245  	{
   246  		f, _ := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
   247  		for y := 1; y < 255; y++ {
   248  			exp := getChunk(15, ^y)
   249  			got, err := f.Retrieve(uint64(y))
   250  			if err != nil {
   251  				t.Fatal(err)
   252  			}
   253  			if !bytes.Equal(got, exp) {
   254  				t.Fatalf("test %d, got \n%x != \n%x", y, got, exp)
   255  			}
   256  		}
   257  	}
   258  }
   259  
   260  // TestSnappyDetection tests that we fail to open a snappy database and vice versa
   261  func TestSnappyDetection(t *testing.T) {
   262  	t.Parallel()
   263  	rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge()
   264  	fname := fmt.Sprintf("snappytest-%d", rand.Uint64())
   265  	// Open with snappy
   266  	{
   267  		f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
   268  		if err != nil {
   269  			t.Fatal(err)
   270  		}
   271  		// Write 15 bytes 255 times
   272  		for x := 0; x < 0xff; x++ {
   273  			data := getChunk(15, x)
   274  			f.Append(uint64(x), data)
   275  		}
   276  		f.Close()
   277  	}
   278  	// Open without snappy
   279  	{
   280  		f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, false)
   281  		if err != nil {
   282  			t.Fatal(err)
   283  		}
   284  		if _, err = f.Retrieve(0); err == nil {
   285  			f.Close()
   286  			t.Fatalf("expected empty table")
   287  		}
   288  	}
   289  
   290  	// Open with snappy
   291  	{
   292  		f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
   293  		if err != nil {
   294  			t.Fatal(err)
   295  		}
   296  		// There should be 255 items
   297  		if _, err = f.Retrieve(0xfe); err != nil {
   298  			f.Close()
   299  			t.Fatalf("expected no error, got %v", err)
   300  		}
   301  	}
   302  
   303  }
   304  func assertFileSize(f string, size int64) error {
   305  	stat, err := os.Stat(f)
   306  	if err != nil {
   307  		return err
   308  	}
   309  	if stat.Size() != size {
   310  		return fmt.Errorf("error, expected size %d, got %d", size, stat.Size())
   311  	}
   312  	return nil
   313  
   314  }
   315  
   316  // TestFreezerRepairDanglingIndex checks that if the index has more entries than there are data,
   317  // the index is repaired
   318  func TestFreezerRepairDanglingIndex(t *testing.T) {
   319  	t.Parallel()
   320  	rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge()
   321  	fname := fmt.Sprintf("dangling_indextest-%d", rand.Uint64())
   322  
   323  	{ // Fill a table and close it
   324  		f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
   325  		if err != nil {
   326  			t.Fatal(err)
   327  		}
   328  		// Write 15 bytes 9 times : 150 bytes
   329  		for x := 0; x < 9; x++ {
   330  			data := getChunk(15, x)
   331  			f.Append(uint64(x), data)
   332  		}
   333  		// The last item should be there
   334  		if _, err = f.Retrieve(f.items - 1); err != nil {
   335  			f.Close()
   336  			t.Fatal(err)
   337  		}
   338  		f.Close()
   339  		// File sizes should be 45, 45, 45 : items[3, 3, 3)
   340  	}
   341  	// Crop third file
   342  	fileToCrop := filepath.Join(os.TempDir(), fmt.Sprintf("%s.0002.rdat", fname))
   343  	// Truncate third file: 45 ,45, 20
   344  	{
   345  		if err := assertFileSize(fileToCrop, 45); err != nil {
   346  			t.Fatal(err)
   347  		}
   348  		file, err := os.OpenFile(fileToCrop, os.O_RDWR, 0644)
   349  		if err != nil {
   350  			t.Fatal(err)
   351  		}
   352  		file.Truncate(20)
   353  		file.Close()
   354  	}
   355  	// Open db it again
   356  	// It should restore the file(s) to
   357  	// 45, 45, 15
   358  	// with 3+3+1 items
   359  	{
   360  		f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
   361  		if err != nil {
   362  			t.Fatal(err)
   363  		}
   364  		if f.items != 7 {
   365  			f.Close()
   366  			t.Fatalf("expected %d items, got %d", 7, f.items)
   367  		}
   368  		if err := assertFileSize(fileToCrop, 15); err != nil {
   369  			t.Fatal(err)
   370  		}
   371  	}
   372  }
   373  
   374  func TestFreezerTruncate(t *testing.T) {
   375  
   376  	t.Parallel()
   377  	rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge()
   378  	fname := fmt.Sprintf("truncation-%d", rand.Uint64())
   379  
   380  	{ // Fill table
   381  		f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
   382  		if err != nil {
   383  			t.Fatal(err)
   384  		}
   385  		// Write 15 bytes 30 times
   386  		for x := 0; x < 30; x++ {
   387  			data := getChunk(15, x)
   388  			f.Append(uint64(x), data)
   389  		}
   390  		// The last item should be there
   391  		if _, err = f.Retrieve(f.items - 1); err != nil {
   392  			t.Fatal(err)
   393  		}
   394  		f.Close()
   395  	}
   396  	// Reopen, truncate
   397  	{
   398  		f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
   399  		if err != nil {
   400  			t.Fatal(err)
   401  		}
   402  		defer f.Close()
   403  		f.truncate(10) // 150 bytes
   404  		if f.items != 10 {
   405  			t.Fatalf("expected %d items, got %d", 10, f.items)
   406  		}
   407  		// 45, 45, 45, 15 -- bytes should be 15
   408  		if f.headBytes != 15 {
   409  			t.Fatalf("expected %d bytes, got %d", 15, f.headBytes)
   410  		}
   411  
   412  	}
   413  
   414  }
   415  
   416  // TestFreezerRepairFirstFile tests a head file with the very first item only half-written.
   417  // That will rewind the index, and _should_ truncate the head file
   418  func TestFreezerRepairFirstFile(t *testing.T) {
   419  	t.Parallel()
   420  	rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge()
   421  	fname := fmt.Sprintf("truncationfirst-%d", rand.Uint64())
   422  	{ // Fill table
   423  		f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
   424  		if err != nil {
   425  			t.Fatal(err)
   426  		}
   427  		// Write 80 bytes, splitting out into two files
   428  		f.Append(0, getChunk(40, 0xFF))
   429  		f.Append(1, getChunk(40, 0xEE))
   430  		// The last item should be there
   431  		if _, err = f.Retrieve(f.items - 1); err != nil {
   432  			t.Fatal(err)
   433  		}
   434  		f.Close()
   435  	}
   436  	// Truncate the file in half
   437  	fileToCrop := filepath.Join(os.TempDir(), fmt.Sprintf("%s.0001.rdat", fname))
   438  	{
   439  		if err := assertFileSize(fileToCrop, 40); err != nil {
   440  			t.Fatal(err)
   441  		}
   442  		file, err := os.OpenFile(fileToCrop, os.O_RDWR, 0644)
   443  		if err != nil {
   444  			t.Fatal(err)
   445  		}
   446  		file.Truncate(20)
   447  		file.Close()
   448  	}
   449  	// Reopen
   450  	{
   451  		f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
   452  		if err != nil {
   453  			t.Fatal(err)
   454  		}
   455  		if f.items != 1 {
   456  			f.Close()
   457  			t.Fatalf("expected %d items, got %d", 0, f.items)
   458  		}
   459  		// Write 40 bytes
   460  		f.Append(1, getChunk(40, 0xDD))
   461  		f.Close()
   462  		// Should have been truncated down to zero and then 40 written
   463  		if err := assertFileSize(fileToCrop, 40); err != nil {
   464  			t.Fatal(err)
   465  		}
   466  	}
   467  }
   468  
   469  // TestFreezerReadAndTruncate tests:
   470  // - we have a table open
   471  // - do some reads, so files are open in readonly
   472  // - truncate so those files are 'removed'
   473  // - check that we did not keep the rdonly file descriptors
   474  func TestFreezerReadAndTruncate(t *testing.T) {
   475  	t.Parallel()
   476  	rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge()
   477  	fname := fmt.Sprintf("read_truncate-%d", rand.Uint64())
   478  	{ // Fill table
   479  		f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
   480  		if err != nil {
   481  			t.Fatal(err)
   482  		}
   483  		// Write 15 bytes 30 times
   484  		for x := 0; x < 30; x++ {
   485  			data := getChunk(15, x)
   486  			f.Append(uint64(x), data)
   487  		}
   488  		// The last item should be there
   489  		if _, err = f.Retrieve(f.items - 1); err != nil {
   490  			t.Fatal(err)
   491  		}
   492  		f.Close()
   493  	}
   494  	// Reopen and read all files
   495  	{
   496  		f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
   497  		if err != nil {
   498  			t.Fatal(err)
   499  		}
   500  		if f.items != 30 {
   501  			f.Close()
   502  			t.Fatalf("expected %d items, got %d", 0, f.items)
   503  		}
   504  		for y := byte(0); y < 30; y++ {
   505  			f.Retrieve(uint64(y))
   506  		}
   507  		// Now, truncate back to zero
   508  		f.truncate(0)
   509  		// Write the data again
   510  		for x := 0; x < 30; x++ {
   511  			data := getChunk(15, ^x)
   512  			if err := f.Append(uint64(x), data); err != nil {
   513  				t.Fatalf("error %v", err)
   514  			}
   515  		}
   516  		f.Close()
   517  	}
   518  }
   519  
   520  func TestOffset(t *testing.T) {
   521  	t.Parallel()
   522  	rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge()
   523  	fname := fmt.Sprintf("offset-%d", rand.Uint64())
   524  	{ // Fill table
   525  		f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 40, true)
   526  		if err != nil {
   527  			t.Fatal(err)
   528  		}
   529  		// Write 6 x 20 bytes, splitting out into three files
   530  		f.Append(0, getChunk(20, 0xFF))
   531  		f.Append(1, getChunk(20, 0xEE))
   532  
   533  		f.Append(2, getChunk(20, 0xdd))
   534  		f.Append(3, getChunk(20, 0xcc))
   535  
   536  		f.Append(4, getChunk(20, 0xbb))
   537  		f.Append(5, getChunk(20, 0xaa))
   538  		f.printIndex()
   539  		f.Close()
   540  	}
   541  	// Now crop it.
   542  	{
   543  		// delete files 0 and 1
   544  		for i := 0; i < 2; i++ {
   545  			p := filepath.Join(os.TempDir(), fmt.Sprintf("%v.%04d.rdat", fname, i))
   546  			if err := os.Remove(p); err != nil {
   547  				t.Fatal(err)
   548  			}
   549  		}
   550  		// Read the index file
   551  		p := filepath.Join(os.TempDir(), fmt.Sprintf("%v.ridx", fname))
   552  		indexFile, err := os.OpenFile(p, os.O_RDWR, 0644)
   553  		if err != nil {
   554  			t.Fatal(err)
   555  		}
   556  		indexBuf := make([]byte, 7*indexEntrySize)
   557  		indexFile.Read(indexBuf)
   558  
   559  		// Update the index file, so that we store
   560  		// [ file = 2, offset = 4 ] at index zero
   561  
   562  		tailId := uint32(2)     // First file is 2
   563  		itemOffset := uint32(4) // We have removed four items
   564  		zeroIndex := indexEntry{
   565  			offset:  tailId,
   566  			filenum: itemOffset,
   567  		}
   568  		buf := zeroIndex.marshallBinary()
   569  		// Overwrite index zero
   570  		copy(indexBuf, buf)
   571  		// Remove the four next indices by overwriting
   572  		copy(indexBuf[indexEntrySize:], indexBuf[indexEntrySize*5:])
   573  		indexFile.WriteAt(indexBuf, 0)
   574  		// Need to truncate the moved index items
   575  		indexFile.Truncate(indexEntrySize * (1 + 2))
   576  		indexFile.Close()
   577  
   578  	}
   579  	// Now open again
   580  	{
   581  		f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 40, true)
   582  		if err != nil {
   583  			t.Fatal(err)
   584  		}
   585  		f.printIndex()
   586  		// It should allow writing item 6
   587  		f.Append(6, getChunk(20, 0x99))
   588  
   589  		// It should be fine to fetch 4,5,6
   590  		if got, err := f.Retrieve(4); err != nil {
   591  			t.Fatal(err)
   592  		} else if exp := getChunk(20, 0xbb); !bytes.Equal(got, exp) {
   593  			t.Fatalf("expected %x got %x", exp, got)
   594  		}
   595  		if got, err := f.Retrieve(5); err != nil {
   596  			t.Fatal(err)
   597  		} else if exp := getChunk(20, 0xaa); !bytes.Equal(got, exp) {
   598  			t.Fatalf("expected %x got %x", exp, got)
   599  		}
   600  		if got, err := f.Retrieve(6); err != nil {
   601  			t.Fatal(err)
   602  		} else if exp := getChunk(20, 0x99); !bytes.Equal(got, exp) {
   603  			t.Fatalf("expected %x got %x", exp, got)
   604  		}
   605  
   606  		// It should error at 0, 1,2,3
   607  		for i := 0; i < 4; i++ {
   608  			if _, err := f.Retrieve(uint64(i)); err == nil {
   609  				t.Fatal("expected err")
   610  			}
   611  		}
   612  	}
   613  }
   614  
   615  // TODO (?)
   616  // - test that if we remove several head-files, aswell as data last data-file,
   617  //   the index is truncated accordingly
   618  // Right now, the freezer would fail on these conditions:
   619  // 1. have data files d0, d1, d2, d3
   620  // 2. remove d2,d3
   621  //
   622  // However, all 'normal' failure modes arising due to failing to sync() or save a file should be
   623  // handled already, and the case described above can only (?) happen if an external process/user
   624  // deletes files from the filesystem.