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 }