summaryrefslogtreecommitdiffstats
path: root/memfilepersistence/src/tests/spi/memfileautorepairtest.cpp
blob: 9984452caed6af3fb3b498ad668a0b451756b2aa (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.

#include <vespa/memfilepersistence/mapper/memfilemapper.h>
#include <vespa/memfilepersistence/mapper/memfile_v1_serializer.h>
#include <vespa/memfilepersistence/mapper/memfile_v1_verifier.h>
#include <tests/spi/memfiletestutils.h>

namespace storage {
namespace memfile {

class MemFileAutoRepairTest : public SingleDiskMemFileTestUtils
{
public:
    void setUp() override;
    void tearDown() override;

    void testFileMetadataCorruptionIsAutoRepaired();
    void testDocumentContentCorruptionIsAutoRepaired();
    void testCorruptionEvictsBucketFromCache();
    void testRepairFailureInMaintainEvictsBucketFromCache();
    void testZeroLengthFileIsDeleted();
    void testTruncatedBodyLocationIsAutoRepaired();
    void testTruncatedHeaderLocationIsAutoRepaired();
    void testTruncatedHeaderBlockIsAutoRepaired();

    void corruptBodyBlock();

    CPPUNIT_TEST_SUITE(MemFileAutoRepairTest);
    CPPUNIT_TEST(testFileMetadataCorruptionIsAutoRepaired);
    CPPUNIT_TEST(testDocumentContentCorruptionIsAutoRepaired);
    CPPUNIT_TEST(testCorruptionEvictsBucketFromCache);
    CPPUNIT_TEST(testRepairFailureInMaintainEvictsBucketFromCache);
    CPPUNIT_TEST(testZeroLengthFileIsDeleted);
    CPPUNIT_TEST(testTruncatedBodyLocationIsAutoRepaired);
    CPPUNIT_TEST(testTruncatedHeaderLocationIsAutoRepaired);
    CPPUNIT_TEST(testTruncatedHeaderBlockIsAutoRepaired);
    CPPUNIT_TEST_SUITE_END();

private:
    void assertDocumentIsSilentlyRemoved(
            const document::BucketId& bucket,
            const document::DocumentId& docId);

    void reconfigureMinimumHeaderBlockSize(uint32_t newMinSize);

    document::BucketId _bucket;
    std::unique_ptr<FileSpecification> _file;
    std::vector<document::DocumentId> _slotIds;
};

CPPUNIT_TEST_SUITE_REGISTRATION(MemFileAutoRepairTest);

namespace {
    // A totall uncached memfile with content to use for verify testing
    std::unique_ptr<MemFile> _memFile;

    // Clear old content. Create new file. Make sure nothing is cached.
    void prepareBucket(SingleDiskMemFileTestUtils& util,
                       const FileSpecification& file) {
        _memFile.reset();
        util.env()._cache.clear();
        vespalib::unlink(file.getPath());
        util.createTestBucket(file.getBucketId(), 0);
        util.env()._cache.clear();
        _memFile.reset(new MemFile(file, util.env()));
        _memFile->getMemFileIO().close();

    }

    MetaSlot getSlot(uint32_t index) {
        assert(_memFile.get());
        vespalib::LazyFile file(_memFile->getFile().getPath(), 0);
        MetaSlot result;
        file.read(&result, sizeof(MetaSlot),
                  sizeof(Header) + sizeof(MetaSlot) * index);
        return result;
    }

    void setSlot(uint32_t index, MetaSlot slot,
                 bool updateFileChecksum = true)
    {
        (void)updateFileChecksum;
        assert(_memFile.get());
        //if (updateFileChecksum) slot.updateFileChecksum();
        vespalib::LazyFile file(_memFile->getFile().getPath(), 0);
        file.write(&slot, sizeof(MetaSlot),
                   sizeof(Header) + sizeof(MetaSlot) * index);
    }
}

void
MemFileAutoRepairTest::setUp()
{
    SingleDiskMemFileTestUtils::setUp();
    _bucket = BucketId(16, 0xa);
    createTestBucket(_bucket, 0);

    {
        MemFilePtr memFilePtr(env()._cache.get(_bucket, env(), env().getDirectory()));
        _file.reset(new FileSpecification(memFilePtr->getFile()));
        CPPUNIT_ASSERT(memFilePtr->getSlotCount() >= 2);
        for (size_t i = 0; i < memFilePtr->getSlotCount(); ++i) {
            _slotIds.push_back(memFilePtr->getDocumentId((*memFilePtr)[i]));
        }
    }
    env()._cache.clear();
}

void
MemFileAutoRepairTest::tearDown()
{
    _file.reset(0);
    _memFile.reset(0);
    SingleDiskMemFileTestUtils::tearDown();
};

void
MemFileAutoRepairTest::testFileMetadataCorruptionIsAutoRepaired()
{
    // Test corruption detected in initial metadata load
    prepareBucket(*this, *_file);
    document::DocumentId id(_slotIds[1]);
    MetaSlot slot(getSlot(1));
    CPPUNIT_ASSERT(slot._gid == id.getGlobalId()); // Sanity checking...
    {
        MetaSlot s(slot);
        s.setTimestamp(Timestamp(40));
        setSlot(1, s);
    }

    CPPUNIT_ASSERT_EQUAL(std::string(""), getModifiedBuckets());
    
    // File not in cache; should be detected in initial load
    spi::GetResult res(doGet(_bucket, id, document::AllFields()));
    // FIXME: currently loadFile is silently fixing corruptions!
    //CPPUNIT_ASSERT_EQUAL(spi::Result::TRANSIENT_ERROR, res.getErrorCode());
    CPPUNIT_ASSERT_EQUAL(spi::Result::NONE, res.getErrorCode());
    CPPUNIT_ASSERT(!res.hasDocument());

    CPPUNIT_ASSERT_EQUAL(std::string("400000000000000a"), getModifiedBuckets());
    CPPUNIT_ASSERT_EQUAL(std::string(""), getModifiedBuckets());

    // File should now have been repaired, so a subsequent get for
    // the same document should just return an empty (but OK) result.
    spi::GetResult res2(doGet(_bucket, id, document::AllFields()));
    CPPUNIT_ASSERT_EQUAL(spi::Result::NONE, res2.getErrorCode());
    CPPUNIT_ASSERT(!res2.hasDocument());

    CPPUNIT_ASSERT_EQUAL(std::string(""), getModifiedBuckets());
}

void
MemFileAutoRepairTest::corruptBodyBlock()
{
    CPPUNIT_ASSERT(!env()._cache.contains(_bucket));
    // Corrupt body block of slot 1
    MetaSlot slot(getSlot(1));
    {
        MetaSlot s(slot);
        s.setBodyPos(52);
        s.setBodySize(18);
        s.updateChecksum();
        setSlot(1, s);
    }
}

void
MemFileAutoRepairTest::testDocumentContentCorruptionIsAutoRepaired()
{
    // Corrupt body block
    prepareBucket(*this, *_file);
    document::DocumentId id(_slotIds[1]);
    corruptBodyBlock();

    CPPUNIT_ASSERT_EQUAL(std::string(""), getModifiedBuckets());
    
    spi::GetResult res(doGet(_bucket, id, document::AllFields()));
    CPPUNIT_ASSERT_EQUAL(spi::Result::TRANSIENT_ERROR, res.getErrorCode());
    CPPUNIT_ASSERT(!res.hasDocument());

    CPPUNIT_ASSERT(!env()._cache.contains(_bucket));

    CPPUNIT_ASSERT_EQUAL(std::string("400000000000000a"), getModifiedBuckets());
    CPPUNIT_ASSERT_EQUAL(std::string(""), getModifiedBuckets());

    // File should now have been repaired, so a subsequent get for
    // the same document should just return an empty (but OK) result.
    spi::GetResult res2(doGet(_bucket, id, document::AllFields()));
    CPPUNIT_ASSERT_EQUAL(spi::Result::NONE, res2.getErrorCode());
    CPPUNIT_ASSERT(!res2.hasDocument());

    // File should now be in cache OK
    CPPUNIT_ASSERT(env()._cache.contains(_bucket));
    CPPUNIT_ASSERT_EQUAL(std::string(""), getModifiedBuckets());
}

// Ideally we'd test this for each spi operation that accesses MemFiles, but
// they all use the same eviction+auto-repair logic...
void
MemFileAutoRepairTest::testCorruptionEvictsBucketFromCache()
{
    prepareBucket(*this, *_file);
    corruptBodyBlock();

    // Read slot 0 and shove file into cache
    spi::GetResult res(doGet(_bucket, _slotIds[0], document::AllFields()));
    CPPUNIT_ASSERT_EQUAL(spi::Result::NONE, res.getErrorCode());
    CPPUNIT_ASSERT(res.hasDocument());
    CPPUNIT_ASSERT(env()._cache.contains(_bucket));

    spi::GetResult res2(doGet(_bucket, _slotIds[1], document::AllFields()));
    CPPUNIT_ASSERT_EQUAL(spi::Result::TRANSIENT_ERROR, res2.getErrorCode());
    CPPUNIT_ASSERT(!res2.hasDocument());

    // Out of the cache! Begone! Shoo!
    CPPUNIT_ASSERT(!env()._cache.contains(_bucket));

}

void
MemFileAutoRepairTest::testRepairFailureInMaintainEvictsBucketFromCache()
{
    prepareBucket(*this, *_file);
    corruptBodyBlock();
    spi::Result result(getPersistenceProvider().maintain(
            spi::Bucket(_bucket, spi::PartitionId(0)), spi::HIGH));
    // File being successfully repaired does not constitute a failure of
    // the maintain() call.
    CPPUNIT_ASSERT_EQUAL(spi::Result::NONE, result.getErrorCode());
    // It should, however, shove it out of the cache.
    CPPUNIT_ASSERT(!env()._cache.contains(_bucket));
}

void
MemFileAutoRepairTest::testZeroLengthFileIsDeleted()
{
    // Completely truncate auto-created file
    vespalib::LazyFile file(_file->getPath(), 0);
    file.resize(0);

    // No way to deal with zero-length files aside from deleting them.
    spi::Result result(getPersistenceProvider().maintain(
            spi::Bucket(_bucket, spi::PartitionId(0)), spi::HIGH));
    CPPUNIT_ASSERT_EQUAL(spi::Result::NONE, result.getErrorCode());
    CPPUNIT_ASSERT(!env()._cache.contains(_bucket));
    CPPUNIT_ASSERT(!vespalib::fileExists(_file->getPath()));
}

namespace {

uint32_t
alignDown(uint32_t value)
{
    uint32_t blocks = value / 512;
    return blocks * 512;
};

FileInfo
fileInfoFromMemFile(const MemFilePtr& mf)
{
    auto& ioBuf(dynamic_cast<const SimpleMemFileIOBuffer&>(
                mf->getMemFileIO()));
    return ioBuf.getFileInfo();
}

}

void
MemFileAutoRepairTest::assertDocumentIsSilentlyRemoved(
        const document::BucketId& bucket,
        const document::DocumentId& docId)
{
    // Corrupted (truncated) slot should be transparently removed during
    // loadFile and it should be as if it was never there!
    spi::Bucket spiBucket(bucket, spi::PartitionId(0));
    spi::GetResult res(doGet(spiBucket, docId, document::AllFields()));
    CPPUNIT_ASSERT_EQUAL(spi::Result::NONE, res.getErrorCode());
    CPPUNIT_ASSERT(!res.hasDocument());
}

void
MemFileAutoRepairTest::testTruncatedBodyLocationIsAutoRepaired()
{
    document::BucketId bucket(16, 4);
    document::Document::SP doc(
            createRandomDocumentAtLocation(4, 1234, 1024, 1024));

    doPut(doc, bucket, framework::MicroSecTime(1000));
    flush(bucket);
    FileInfo fileInfo;
    {
        MemFilePtr mf(getMemFile(bucket));
        CPPUNIT_ASSERT_EQUAL(uint32_t(1), mf->getSlotCount());
        fileInfo = fileInfoFromMemFile(mf);

        const uint32_t bodyBlockStart(
                sizeof(Header)
                + fileInfo._metaDataListSize * sizeof(MetaSlot)
                + fileInfo._headerBlockSize);

        vespalib::LazyFile file(mf->getFile().getPath(), 0);
        uint32_t slotBodySize = (*mf)[0].getLocation(BODY)._size;
        CPPUNIT_ASSERT(slotBodySize > 0);
        // Align down to nearest sector alignment to avoid unrelated DirectIO
        // checks to kick in. Since the body block is always aligned on a
        // sector boundary, we know this cannot truncate into the header block.
        file.resize(alignDown(bodyBlockStart + slotBodySize - 1));
    }
    env()._cache.clear();
    assertDocumentIsSilentlyRemoved(bucket, doc->getId());
}

void
MemFileAutoRepairTest::testTruncatedHeaderLocationIsAutoRepaired()
{
    document::BucketId bucket(16, 4);
    document::Document::SP doc(
            createRandomDocumentAtLocation(4, 1234, 1024, 1024));
    // Ensure header has a bunch of data (see alignment comments below).
    doc->setValue(doc->getField("hstringval"),
                  document::StringFieldValue(std::string(1024, 'A')));

    doPut(doc, bucket, framework::MicroSecTime(1000));
    flush(bucket);
    FileInfo fileInfo;
    {
        MemFilePtr mf(getMemFile(bucket));
        CPPUNIT_ASSERT_EQUAL(uint32_t(1), mf->getSlotCount());
        fileInfo = fileInfoFromMemFile(mf);

        const uint32_t headerBlockStart(
                sizeof(Header)
                + fileInfo._metaDataListSize * sizeof(MetaSlot));

        vespalib::LazyFile file(mf->getFile().getPath(), 0);
        uint32_t slotHeaderSize = (*mf)[0].getLocation(HEADER)._size;
        CPPUNIT_ASSERT(slotHeaderSize > 0);
        // Align down to nearest sector alignment to avoid unrelated DirectIO
        // checks to kick in. The header block is not guaranteed to start on
        // sector boundary, but we assume there is enough slack in the header
        // section for the metadata slots themselves to be untouched since we
        // have a minimum header size of 1024 for the doc in question.
        file.resize(alignDown(headerBlockStart + slotHeaderSize - 1));
    }
    env()._cache.clear();
    assertDocumentIsSilentlyRemoved(bucket, doc->getId());
}

void
MemFileAutoRepairTest::reconfigureMinimumHeaderBlockSize(uint32_t newMinSize)
{
    using MemFileConfig = vespa::config::storage::StorMemfilepersistenceConfig;
    using MemFileConfigBuilder
        = vespa::config::storage::StorMemfilepersistenceConfigBuilder;
    MemFileConfigBuilder builder(
            *env().acquireConfigReadLock().memFilePersistenceConfig());
    builder.minimumFileMetaSlots = 2;
    builder.minimumFileHeaderBlockSize = newMinSize;
    auto newConfig = std::unique_ptr<MemFileConfig>(new MemFileConfig(builder));
    env().acquireConfigWriteLock().setMemFilePersistenceConfig(
            std::move(newConfig));
}

void
MemFileAutoRepairTest::testTruncatedHeaderBlockIsAutoRepaired()
{
    document::BucketId bucket(16, 4);
    document::Document::SP doc(
            createRandomDocumentAtLocation(4, 1234, 1, 1));
    // Ensure header block is large enough that free space is added to the end.
    reconfigureMinimumHeaderBlockSize(8192);
    // Add header field and remove randomly generated body field, ensuring
    // we have no data to add to body field. This will prevent slot body
    // location checking from detecting a header truncation.
    doc->setValue(doc->getField("hstringval"),
                  document::StringFieldValue("foo"));
    doc->remove(doc->getField("content"));

    doPut(doc, bucket, framework::MicroSecTime(1000));
    flush(bucket);
    FileInfo fileInfo;
    {
        MemFilePtr mf(getMemFile(bucket));
        CPPUNIT_ASSERT_EQUAL(uint32_t(1), mf->getSlotCount());
        fileInfo = fileInfoFromMemFile(mf);

        const uint32_t headerBlockEnd(
                sizeof(Header)
                + fileInfo._metaDataListSize * sizeof(MetaSlot)
                + fileInfo._headerBlockSize);

        vespalib::LazyFile file(mf->getFile().getPath(), 0);
        CPPUNIT_ASSERT_EQUAL(uint32_t(0),
                             (*mf)[0].getLocation(BODY)._size); // No body.
        const auto headerLoc((*mf)[0].getLocation(HEADER));
        const uint32_t extent(headerLoc._pos + headerLoc._size);
        // Make sure we don't intersect an existing slot range.
        CPPUNIT_ASSERT(extent < alignDown(headerBlockEnd - 1));
        file.resize(alignDown(headerBlockEnd - 1));
    }
    env()._cache.clear();
    assertDocumentIsSilentlyRemoved(bucket, doc->getId());
}

}
}