github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/store/nbs/journal_record_test.go (about)

     1  // Copyright 2022 Dolthub, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package nbs
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"math/rand"
    21  	"testing"
    22  	"time"
    23  
    24  	"github.com/stretchr/testify/assert"
    25  	"github.com/stretchr/testify/require"
    26  
    27  	"github.com/dolthub/dolt/go/store/chunks"
    28  	"github.com/dolthub/dolt/go/store/d"
    29  	"github.com/dolthub/dolt/go/store/hash"
    30  )
    31  
    32  func testTimestampGenerator() uint64 {
    33  	return 42
    34  }
    35  
    36  func TestRoundTripJournalRecords(t *testing.T) {
    37  	t.Run("chunk record", func(t *testing.T) {
    38  		for i := 0; i < 64; i++ {
    39  			rec, buf := makeChunkRecord()
    40  			assert.Equal(t, rec.length, uint32(len(buf)))
    41  			b := make([]byte, rec.length)
    42  			n := writeChunkRecord(b, mustCompressedChunk(rec))
    43  			assert.Equal(t, n, rec.length)
    44  			assert.Equal(t, buf, b)
    45  			r, err := readJournalRecord(buf)
    46  			assert.NoError(t, err)
    47  			assert.Equal(t, rec, r)
    48  		}
    49  	})
    50  
    51  	// Root hash records contain a timestamp, so override the journal record timestamp
    52  	// generator function with a test version that returns a known, predictable value.
    53  	journalRecordTimestampGenerator = testTimestampGenerator
    54  
    55  	t.Run("root hash record", func(t *testing.T) {
    56  		for i := 0; i < 64; i++ {
    57  			rec, buf := makeRootHashRecord()
    58  			assert.Equal(t, rec.length, uint32(len(buf)))
    59  			b := make([]byte, rec.length)
    60  			n := writeRootHashRecord(b, rec.address)
    61  			assert.Equal(t, n, rec.length)
    62  			assert.Equal(t, buf, b)
    63  			r, err := readJournalRecord(buf)
    64  			assert.NoError(t, err)
    65  			assert.Equal(t, rec, r)
    66  		}
    67  	})
    68  }
    69  
    70  func TestUnknownJournalRecordTag(t *testing.T) {
    71  	// test behavior encountering unknown tag
    72  	buf := makeUnknownTagJournalRecord()
    73  	// checksum is ok
    74  	ok := validateJournalRecord(buf)
    75  	assert.True(t, ok)
    76  	// reading record fails
    77  	_, err := readJournalRecord(buf)
    78  	assert.Error(t, err)
    79  }
    80  
    81  func TestProcessJournalRecords(t *testing.T) {
    82  	const cnt = 1024
    83  	ctx := context.Background()
    84  	records := make([]journalRec, cnt)
    85  	buffers := make([][]byte, cnt)
    86  	journal := make([]byte, cnt*1024)
    87  
    88  	// Root hash records contain a timestamp, so override the journal record timestamp
    89  	// generator function with a test version that returns a known, predictable value.
    90  	journalRecordTimestampGenerator = testTimestampGenerator
    91  
    92  	var off uint32
    93  	for i := range records {
    94  		var r journalRec
    95  		var b []byte
    96  		if i%8 == 0 {
    97  			r, b = makeRootHashRecord()
    98  			off += writeRootHashRecord(journal[off:], r.address)
    99  		} else {
   100  			r, b = makeChunkRecord()
   101  			off += writeChunkRecord(journal[off:], mustCompressedChunk(r))
   102  		}
   103  		records[i], buffers[i] = r, b
   104  	}
   105  
   106  	var i, sum int
   107  	check := func(o int64, r journalRec) (_ error) {
   108  		require.True(t, i < cnt)
   109  		assert.Equal(t, records[i], r)
   110  		assert.Equal(t, sum, int(o))
   111  		sum += len(buffers[i])
   112  		i++
   113  		return
   114  	}
   115  
   116  	n, err := processJournalRecords(ctx, bytes.NewReader(journal), 0, check)
   117  	assert.Equal(t, cnt, i)
   118  	assert.Equal(t, int(off), int(n))
   119  	require.NoError(t, err)
   120  
   121  	i, sum = 0, 0
   122  	// write a bogus record to the end and process again
   123  	writeCorruptJournalRecord(journal[off:])
   124  	n, err = processJournalRecords(ctx, bytes.NewReader(journal), 0, check)
   125  	assert.Equal(t, cnt, i)
   126  	assert.Equal(t, int(off), int(n))
   127  	require.NoError(t, err)
   128  }
   129  
   130  func randomMemTable(cnt int) (*memTable, map[hash.Hash]chunks.Chunk) {
   131  	chnx := make(map[hash.Hash]chunks.Chunk, cnt)
   132  	for i := 0; i < cnt; i++ {
   133  		ch := chunks.NewChunk(randBuf(100))
   134  		chnx[ch.Hash()] = ch
   135  	}
   136  	mt := newMemTable(uint64(cnt) * 256)
   137  	for a, ch := range chnx {
   138  		mt.addChunk(a, ch.Data())
   139  	}
   140  	return mt, chnx
   141  }
   142  
   143  func makeChunkRecord() (journalRec, []byte) {
   144  	ch := chunks.NewChunk(randBuf(100))
   145  	cc := ChunkToCompressedChunk(ch)
   146  	payload := cc.FullCompressedChunk
   147  	sz, _ := chunkRecordSize(cc)
   148  
   149  	var n int
   150  	buf := make([]byte, sz)
   151  	// length
   152  	writeUint32(buf[n:], uint32(len(buf)))
   153  	n += journalRecLenSz
   154  	// kind
   155  	buf[n] = byte(kindJournalRecTag)
   156  	n += journalRecTagSz
   157  	buf[n] = byte(chunkJournalRecKind)
   158  	n += journalRecKindSz
   159  	// address
   160  	buf[n] = byte(addrJournalRecTag)
   161  	n += journalRecTagSz
   162  	copy(buf[n:], cc.H[:])
   163  	n += journalRecAddrSz
   164  	// payload
   165  	buf[n] = byte(payloadJournalRecTag)
   166  	n += journalRecTagSz
   167  	copy(buf[n:], payload)
   168  	n += len(payload)
   169  	// checksum
   170  	c := crc(buf[:len(buf)-journalRecChecksumSz])
   171  	writeUint32(buf[len(buf)-journalRecChecksumSz:], c)
   172  
   173  	r := journalRec{
   174  		length:   uint32(len(buf)),
   175  		kind:     chunkJournalRecKind,
   176  		address:  cc.H,
   177  		payload:  payload,
   178  		checksum: c,
   179  	}
   180  	return r, buf
   181  }
   182  
   183  func makeRootHashRecord() (journalRec, []byte) {
   184  	a := hash.Of(randBuf(8))
   185  	var n int
   186  	buf := make([]byte, rootHashRecordSize())
   187  	// length
   188  	writeUint32(buf[n:], uint32(len(buf)))
   189  	n += journalRecLenSz
   190  	// kind
   191  	buf[n] = byte(kindJournalRecTag)
   192  	n += journalRecTagSz
   193  	buf[n] = byte(rootHashJournalRecKind)
   194  	n += journalRecKindSz
   195  	// timestamp
   196  	buf[n] = byte(timestampJournalRecTag)
   197  	n += journalRecTagSz
   198  	writeUint64(buf[n:], testTimestampGenerator())
   199  	n += journalRecTimestampSz
   200  	// address
   201  	buf[n] = byte(addrJournalRecTag)
   202  	n += journalRecTagSz
   203  	copy(buf[n:], a[:])
   204  	n += journalRecAddrSz
   205  	// checksum
   206  	c := crc(buf[:len(buf)-journalRecChecksumSz])
   207  	writeUint32(buf[len(buf)-journalRecChecksumSz:], c)
   208  	r := journalRec{
   209  		length:    uint32(len(buf)),
   210  		kind:      rootHashJournalRecKind,
   211  		address:   a,
   212  		checksum:  c,
   213  		timestamp: time.Unix(int64(testTimestampGenerator()), 0),
   214  	}
   215  	return r, buf
   216  }
   217  
   218  func makeUnknownTagJournalRecord() (buf []byte) {
   219  	const fakeTag journalRecTag = 111
   220  	_, buf = makeRootHashRecord()
   221  	// overwrite recKind
   222  	buf[journalRecLenSz] = byte(fakeTag)
   223  	// redo checksum
   224  	c := crc(buf[:len(buf)-journalRecChecksumSz])
   225  	writeUint32(buf[len(buf)-journalRecChecksumSz:], c)
   226  	return
   227  }
   228  
   229  func writeCorruptJournalRecord(buf []byte) (n uint32) {
   230  	n = uint32(rootHashRecordSize())
   231  	// fill with random data
   232  	rand.Read(buf[:n])
   233  	// write a valid size, kind
   234  	writeUint32(buf, n)
   235  	buf[journalRecLenSz] = byte(rootHashJournalRecKind)
   236  	return
   237  }
   238  
   239  func mustCompressedChunk(rec journalRec) CompressedChunk {
   240  	d.PanicIfFalse(rec.kind == chunkJournalRecKind)
   241  	cc, err := NewCompressedChunk(hash.Hash(rec.address), rec.payload)
   242  	d.PanicIfError(err)
   243  	return cc
   244  }