github.com/aigarnetwork/aigar@v0.0.0-20191115204914-d59a6eb70f8e/core/rawdb/freezer_table_test.go (about)

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