DbEngine2.java

1
package com.renomad.minum.database;
2
3
import com.renomad.minum.state.Context;
4
import com.renomad.minum.utils.CryptoUtils;
5
import com.renomad.minum.utils.FileUtils;
6
import com.renomad.minum.utils.IFileUtils;
7
8
import java.io.IOException;
9
import java.nio.charset.StandardCharsets;
10
import java.nio.file.Path;
11
import java.security.MessageDigest;
12
import java.text.ParseException;
13
import java.util.*;
14
import java.util.concurrent.atomic.AtomicInteger;
15
import java.util.concurrent.atomic.AtomicLong;
16
import java.util.concurrent.locks.ReentrantLock;
17
import java.util.function.Function;
18
import java.util.stream.Stream;
19
20
import static com.renomad.minum.database.ChecksumUtility.generateChecksumErrorMessage;
21
import static com.renomad.minum.database.ChecksumUtility.getMessageDigest;
22
import static com.renomad.minum.database.InspectableLock.getLockOwnerIdString;
23
import static com.renomad.minum.utils.Invariants.mustBeFalse;
24
import static com.renomad.minum.utils.Invariants.mustBeTrue;
25
26
/**
27
 * a memory-based disk-persisted database class.
28
 *
29
 * <p>
30
 *     Engine 2 is a database engine that improves on the performance from the first
31
 *     database provided by Minum. It does this by using different strategies for disk persistence.
32
 * </p>
33
 * <p>
34
 *     The mental model of the previous Minum database has been an in-memory data
35
 *     structure in which every change is eventually written to its own file on disk for
36
 *     persistence.  Data changes affect just their relevant files.  The benefit of this approach is
37
 *     extreme simplicity. It requires very little code, relying as it does on the operating system's file capabilities.
38
 * </p>
39
 * <p>
40
 *     However, there are two performance problems with this approach.  First is when the
41
 *     data changes are arriving at a high rate.  In that situation, the in-memory portion keeps up to date,
42
 *     but the disk portion may lag by minutes.  The second problem is start-up time.  When
43
 *     the database starts, it reads files into memory.  The database can read about 6,000
44
 *     files a second in the best case.  If there are a million data items, it would take
45
 *     about 160 seconds to load it into memory, which is far too long.
46
 * </p>
47
 * <p>
48
 *      The new approach to disk persistence is to append each change to a file.  Append-only file
49
 *      changes can be very fast.  These append files are eventually consolidated into files
50
 *      partitioned by their index - data with indexes between 1 and 1000 go into one file, between
51
 *      1001 and 2000 go into another, and so on.
52
 *  </p>
53
 *  <p>
54
 *      Startup is magnitudes faster by this approach.  What took the previous database 160 seconds
55
 *      to load requires only 2 seconds. Writes to disk are also faster. What would have taken
56
 *      several minutes to write should only take a few seconds now.
57
 *  </p>
58
 *  <p>
59
 *      This new approach uses a different file structure than the previous. If it is
60
 *      desired to use the new engine on existing data, it is possible to convert the old
61
 *      data format to the new.  Construct an instance of the new engine, pointing
62
 *      at the same name as the previous, and it will convert the data.  If the previous
63
 *      call looked like this:
64
 *  </p>
65
 *  {@code
66
 *  Db<Photograph> photoDb = context.getDb("photos", Photograph.EMPTY);
67
 *  }
68
 *  <p>
69
 *  Then converting to the new database is just replacing it with the following
70
 *  line. <b>Please, backup your database before this change.</b>
71
 *  </p>
72
 *  <p>
73
 * {@code
74
 *     DbEngine2<Photograph> photoDb = context.getDb2("photos", Photograph.EMPTY);
75
 * }
76
 *  </p>
77
 *  <p>
78
 *     Once the new engine starts up, it will notice the old file structure and convert it
79
 *     over.  The methods and behaviors are mostly the same between the old and new engines, so the
80
 *     update should be straightforward.
81
 * </p>
82
 * <p>
83
 *     (By the way, it *is* possible to convert back to the old file structure,
84
 *     by starting the database the old way again.  Just be aware that each time the
85
 *     files are converted, it takes longer than normal to start the database)
86
 * </p>
87
 * <p>
88
 *     However, something to note is that using the old database is still fine in many cases,
89
 *     particularly for prototypes or systems which do not contain large amounts of data. If
90
 *     your system is working fine, there is no need to change things.
91
 * </p>
92
 *
93
 * @param <T> the type of data we'll be persisting (must extend from {@link DbData})
94
 */
95
public final class DbEngine2<T extends DbData<?>> extends AbstractDb<T> {
96
97
    private final ReentrantLock loadDataLock;
98
    private final ReentrantLock consolidateLock;
99
100
    /**
101
     * The maximum count of lines going into an append-only file before we consolidate
102
     */
103
    int maxLinesPerAppendFile;
104
105
    /**
106
     * Whether this database has loaded data.  It is preferable to load data
107
     * immediately after initializing the database, using the {@link #loadData()}
108
     * method, so that any failures will bubble up to the main method and make
109
     * the error obvious.
110
     */
111
    boolean hasLoadedData;
112
113
    final DatabaseAppender databaseAppender;
114
    final DatabaseConsolidator databaseConsolidator;
115
116
    /**
117
     * Here we track the number of appends we have made.  Once it hits
118
     * a certain number, we will kick off a consolidation in a thread
119
     */
120
    final AtomicInteger appendCount = new AtomicInteger(0);
121
122
    /**
123
     * Used to determine whether to kick off consolidation.  If it is
124
     * already running, we don't want to kick it off again. This would
125
     * only affect us if we are updating the database very fast.
126
     */
127
    boolean consolidationIsRunning;
128
129
    /**
130
     * Constructs an in-memory disk-persisted database.
131
     * Loading of data from disk happens at the first invocation of any command
132
     * changing or requesting data, such as {@link #write(DbData)}, {@link #delete(DbData)},
133
     * or {@link #values()}.  See the private method loadData() for details.
134
     * @param dbDirectory this uniquely names your database, and also sets the directory
135
     *                    name for this data.  The expected use case is to name this after
136
     *                    the data in question.  For example, "users", or "accounts".
137
     * @param context used to provide important state data to several components
138
     * @param instance an instance of the {@link DbData} object relevant for use in this database. Note
139
     *                 that each database (that is, each instance of this class), focuses on just one
140
     *                 data, which must be an implementation of {@link DbData}.
141
     */
142
    public DbEngine2(Path dbDirectory, Context context, T instance) {
143
        this(dbDirectory, context, instance, new FileUtils(context.getLogger(), context.getConstants()));
144
    }
145
146
    DbEngine2(Path dbDirectory, Context context, T instance, IFileUtils fileUtils) {
147
        super(dbDirectory, context, instance, fileUtils);
148
149
        try {
150
            this.databaseConsolidator = new DatabaseConsolidator(dbDirectory, context, fileUtils);
151
            this.databaseAppender = new DatabaseAppender(dbDirectory, context, fileUtils);
152
        } catch (IOException ex) {
153
            throw new DbException("Error in DbEngine2 constructor", ex);
154
        }
155
        this.loadDataLock = new ReentrantLock();
156
        this.consolidateLock = new ReentrantLock();
157
        this.maxLinesPerAppendFile = context.getConstants().maxAppendCount;
158
    }
159
160
    /**
161
     * Write data to the database.  Use an index of 0 to store new data, and a positive
162
     * non-zero value to update data.
163
     * <p><em>
164
     *     Example of adding new data to the database:
165
     * </em></p>
166
     * {@snippet :
167
     *          final var newSalt = StringUtils.generateSecureRandomString(10);
168
     *          final var hashedPassword = CryptoUtils.createPasswordHash(newPassword, newSalt);
169
     *          final var newUser = new User(0L, newUsername, hashedPassword, newSalt);
170
     *          userDb.write(newUser);
171
     * }
172
     * <p><em>
173
     *     Example of updating data:
174
     * </em></p>
175
     * {@snippet :
176
     *         // write the updated salted password to the database
177
     *         final var updatedUser = new User(
178
     *                 user().getIndex(),
179
     *                 user().getUsername(),
180
     *                 hashedPassword,
181
     *                 newSalt);
182
     *         userDb.write(updatedUser);
183
     * }
184
     *
185
     * @param newData the data we are writing
186
     * @return the data with its new index assigned.
187
     * @throws DbException if there is a failure to write
188
     */
189
    @Override
190
    public T write(T newData) {
191 1 1. write : removed call to com/renomad/minum/database/DbEngine2::basicDataChecks → KILLED
        basicDataChecks(newData);
192
193
        // load data if needed
194 1 1. write : negated conditional → KILLED
        if (!hasLoadedData) loadData();
195
196
        boolean hasBeenBLocked;
197
        long startWaitingTime;
198
        long threadId = Thread.currentThread().threadId();
199 1 1. write : negated conditional → KILLED
        if (!dbLock.tryLock()) {
200
            Thread lockOwner = dbLock.getLockOwner();
201
            logger.logTrace(() -> "Thread %d encountered a lock held by Thread %s during write()".formatted(threadId, getLockOwnerIdString(lockOwner)));
202
            hasBeenBLocked = true;
203
            startWaitingTime = System.currentTimeMillis();
204 1 1. write : removed call to com/renomad/minum/database/InspectableLock::lock → KILLED
            dbLock.lock();
205
        } else {
206
            hasBeenBLocked = false;
207
            startWaitingTime = 0;
208
            logger.logTrace(() -> "Thread %d has acquired the dbLock lock for DbEngine2.write()".formatted(threadId));
209
        }
210
211 1 1. write : negated conditional → KILLED
        if (hasBeenBLocked) {
212 1 1. write : Replaced long subtraction with addition → SURVIVED
            long waitTimeMillis = System.currentTimeMillis() - startWaitingTime;
213
            logger.logTrace(() -> "Thread %d successfully acquired the lock after waiting %d milliseconds".formatted(threadId, waitTimeMillis));
214
        }
215
        long startProcessingTime = System.currentTimeMillis();
216
        try {
217
            boolean newElementCreated = processDataIndex(newData);
218 1 1. write : removed call to com/renomad/minum/database/DbEngine2::writeToDisk → KILLED
            writeToDisk(newData);
219 1 1. write : removed call to com/renomad/minum/database/DbEngine2::writeToMemory → KILLED
            writeToMemory(newData, newElementCreated);
220
        } catch (Exception ex) {
221
           throw new DbException("failed to write data " + newData, ex);
222
        } finally {
223 1 1. write : Replaced long subtraction with addition → KILLED
            long processingTime = System.currentTimeMillis() - startProcessingTime;
224
            logger.logTrace(() -> "Thread %d releasing lock for writing.  Time taken in millis: %d".formatted(Thread.currentThread().threadId(), processingTime));
225 1 1. write : removed call to com/renomad/minum/database/InspectableLock::unlock → KILLED
            dbLock.unlock();
226
        }
227
228
        // returning the data at this point is the most convenient
229
        // way users will have access to the new index of the data.
230 1 1. write : replaced return value with null for com/renomad/minum/database/DbEngine2::write → KILLED
        return newData;
231
    }
232
233
    private void writeToDisk(T newData) throws IOException {
234
        logger.logTrace(() -> String.format("Thread %d is writing data to disk: %s", Thread.currentThread().threadId(), newData));
235
        String serializedData = newData.serialize();
236
        mustBeFalse(serializedData == null || serializedData.isBlank(),
237
                "the serialized form of data must not be blank. " +
238
                        "Is the serialization code written properly? Our datatype: " + emptyInstance);
239
        databaseAppender.appendToDatabase(DatabaseChangeAction.UPDATE, serializedData);
240
        appendCount.incrementAndGet();
241
        consolidateIfNecessary();
242
    }
243
244
    /**
245
     * If the append count is large enough, we will call the
246
     * consolidation method on the DatabaseConsolidator and
247
     * reset the append count to 0.
248
     */
249
    boolean consolidateIfNecessary() {
250 3 1. consolidateIfNecessary : changed conditional boundary → KILLED
2. consolidateIfNecessary : negated conditional → KILLED
3. consolidateIfNecessary : negated conditional → KILLED
        if (appendCount.get() > maxLinesPerAppendFile && !consolidationIsRunning) {
251 1 1. consolidateIfNecessary : removed call to java/util/concurrent/locks/ReentrantLock::lock → KILLED
            consolidateLock.lock(); // block threads here if multiple are trying to get in - only one gets in at a time
252
            try {
253 1 1. consolidateIfNecessary : removed call to com/renomad/minum/database/DbEngine2::consolidateInnerCode → TIMED_OUT
                consolidateInnerCode();
254
            } finally {
255 1 1. consolidateIfNecessary : removed call to java/util/concurrent/locks/ReentrantLock::unlock → SURVIVED
                consolidateLock.unlock();
256
            }
257 1 1. consolidateIfNecessary : replaced boolean return with false for com/renomad/minum/database/DbEngine2::consolidateIfNecessary → TIMED_OUT
            return true;
258
        }
259 1 1. consolidateIfNecessary : replaced boolean return with true for com/renomad/minum/database/DbEngine2::consolidateIfNecessary → KILLED
        return false;
260
    }
261
262
    /**
263
     * This code is only called in production from {@link #consolidateIfNecessary()},
264
     * and is necessarily protected by mutex locks.  However, it is provided
265
     * here as its own method for ease of testing.
266
     */
267
    void consolidateInnerCode() {
268 3 1. consolidateInnerCode : negated conditional → TIMED_OUT
2. consolidateInnerCode : negated conditional → KILLED
3. consolidateInnerCode : changed conditional boundary → KILLED
        if (appendCount.get() > maxLinesPerAppendFile && !consolidationIsRunning) {
269
            context.getExecutorService().submit(() -> {
270
                try {
271
                    consolidationIsRunning = true;
272 1 1. lambda$consolidateInnerCode$6 : removed call to com/renomad/minum/database/DatabaseConsolidator::consolidate → KILLED
                    databaseConsolidator.consolidate();
273
                    consolidationIsRunning = false;
274
                } catch (Exception e) {
275
                    logger.logAsyncError(() -> "Error during consolidation: " + e);
276
                }
277
            });
278 1 1. consolidateInnerCode : removed call to java/util/concurrent/atomic/AtomicInteger::set → KILLED
            appendCount.set(0);
279
        }
280
    }
281
282
    /**
283
     * Delete data
284
     * <p><em>Example:</em></p>
285
     * {@snippet :
286
     *      userDb.delete(user);
287
     * }
288
     * @param dataToDelete the data we are serializing and writing
289
     * @throws DbException if there is a failure to delete
290
     */
291
    @Override
292
    public void delete(T dataToDelete) {
293 1 1. delete : removed call to com/renomad/minum/database/DbEngine2::basicDataChecks → KILLED
        basicDataChecks(dataToDelete);
294
295
        // load data if needed
296 1 1. delete : negated conditional → TIMED_OUT
        if (!hasLoadedData) loadData();
297
298
        boolean hasBeenBLocked;
299
        long startWaitingTime;
300
        long threadId = Thread.currentThread().threadId();
301 1 1. delete : negated conditional → KILLED
        if (!dbLock.tryLock()) {
302
            Thread lockOwner = dbLock.getLockOwner();
303
            logger.logTrace(() -> "Thread %d encountered a lock held by Thread %s during delete()".formatted(threadId, getLockOwnerIdString(lockOwner)));
304
            hasBeenBLocked = true;
305
            startWaitingTime = System.currentTimeMillis();
306 1 1. delete : removed call to com/renomad/minum/database/InspectableLock::lock → SURVIVED
            dbLock.lock();
307
        } else {
308
            hasBeenBLocked = false;
309
            startWaitingTime = 0;
310
            logger.logTrace(() -> "Thread %d has acquired the dbLock lock for DbEngine2.delete()".formatted(threadId));
311
        }
312
313 1 1. delete : negated conditional → TIMED_OUT
        if (hasBeenBLocked) {
314 1 1. delete : Replaced long subtraction with addition → SURVIVED
            long waitTimeMillis = System.currentTimeMillis() - startWaitingTime;
315
            logger.logTrace(() -> "Thread %d successfully acquired the lock after waiting %d milliseconds".formatted(threadId, waitTimeMillis));
316
        }
317
        long startProcessingTime = System.currentTimeMillis();
318
        try {
319 1 1. delete : removed call to com/renomad/minum/database/DbEngine2::deleteFromDisk → KILLED
            deleteFromDisk(dataToDelete);
320 1 1. delete : removed call to com/renomad/minum/database/DbEngine2::deleteFromMemory → KILLED
            deleteFromMemory(dataToDelete);
321
        } catch (Exception ex) {
322
            throw new DbException("failed to delete data " + dataToDelete, ex);
323
        } finally {
324 1 1. delete : Replaced long subtraction with addition → TIMED_OUT
            long processingTime = System.currentTimeMillis() - startProcessingTime;
325
            logger.logTrace(() -> "Thread %d releasing lock for writing.  Time taken in millis: %d".formatted(Thread.currentThread().threadId(), processingTime));
326 1 1. delete : removed call to com/renomad/minum/database/InspectableLock::unlock → TIMED_OUT
            dbLock.unlock();
327
        }
328
    }
329
330
    private void deleteFromDisk(T dataToDelete) throws IOException {
331
        logger.logTrace(() -> String.format("Thread %d deleting data from disk: %s", Thread.currentThread().threadId(), dataToDelete));
332
        databaseAppender.appendToDatabase(DatabaseChangeAction.DELETE, dataToDelete.serialize());
333
        appendCount.incrementAndGet();
334
        consolidateIfNecessary();
335
    }
336
337
    private void loadDataFromDisk() throws IOException, ParseException {
338
        logger.logDebug(() -> "Loading data from disk. Db Engine2. Directory: " + dbDirectory);
339
340
        // if we find the "index.ddps" file, it means we are looking at an old
341
        // version of the database.  Update it to the new version, and then afterwards
342
        // remove the old version files.
343 1 1. loadDataFromDisk : negated conditional → KILLED
        if (fileUtils.exists(dbDirectory.resolve("index.ddps"))) {
344 1 1. loadDataFromDisk : removed call to com/renomad/minum/database/DbFileConverter::convertClassicFolderStructureToDbEngine2Form → KILLED
            new DbFileConverter(context, dbDirectory, fileUtils).convertClassicFolderStructureToDbEngine2Form();
345
        }
346
347
        // if there are any remaining items in the current append-only file, move them
348
        // to a new file
349
        databaseAppender.saveOffCurrentDataToReadyFolder();
350 1 1. loadDataFromDisk : removed call to com/renomad/minum/database/DatabaseAppender::flush → KILLED
        databaseAppender.flush();
351
352
        // consolidate whatever files still exist in the append logs
353 1 1. loadDataFromDisk : removed call to com/renomad/minum/database/DatabaseConsolidator::consolidate → KILLED
        databaseConsolidator.consolidate();
354
355
        // load the data into memory
356 1 1. loadDataFromDisk : removed call to com/renomad/minum/database/DbEngine2::walkAndLoad → KILLED
        walkAndLoad(dbDirectory);
357
358 1 1. loadDataFromDisk : negated conditional → KILLED
        if (data.isEmpty()) {
359
            this.index = new AtomicLong(1);
360
        } else {
361 1 1. loadDataFromDisk : Replaced long addition with subtraction → KILLED
            var initialIndex = Collections.max(data.keySet()) + 1L;
362
            this.index = new AtomicLong(initialIndex);
363
        }
364
    }
365
366
    /**
367
     * Loops through each line of data in the consolidated data files,
368
     * converting each to its strongly-typed form and adding to the database
369
     */
370
    void walkAndLoad(Path dbDirectory) {
371
        List<String> consolidatedFiles = new ArrayList<>(
372
                Arrays.stream(Objects.requireNonNull(
373
                        dbDirectory.resolve("consolidated_data").toFile().list()))
374 2 1. lambda$walkAndLoad$13 : replaced boolean return with true for com/renomad/minum/database/DbEngine2::lambda$walkAndLoad$13 → KILLED
2. lambda$walkAndLoad$13 : negated conditional → KILLED
                        .filter(x -> !x.contains("checksum"))
375
                        .toList());
376
377
        // if there aren't any files, bail out
378 1 1. walkAndLoad : negated conditional → KILLED
        if (consolidatedFiles.isEmpty()) return;
379
380
        // sort
381 1 1. walkAndLoad : removed call to java/util/List::sort → TIMED_OUT
        consolidatedFiles.sort(Comparator.comparingLong(DbEngine2::parseConsolidatedFileName));
382
383
        for (String fileName : consolidatedFiles) {
384
            logger.logDebug(() -> "Processing database file: " + fileName);
385
            Path consolidatedDataFile = dbDirectory.resolve("consolidated_data").resolve(fileName);
386
            Path checksumFilename = consolidatedDataFile.resolveSibling(consolidatedDataFile.getFileName() + ".checksum");
387
388
389
            // By using a lazy stream, we are able to read each item from the file into
390
            // memory without needing to read the whole file contents into memory at once,
391
            // thus avoiding requiring a great amount of memory
392
            // build a hash for this data
393
            MessageDigest messageDigestSha256 = getMessageDigest("SHA-256");
394
395
            try(Stream<String> fileStream = fileUtils.lines(consolidatedDataFile, StandardCharsets.US_ASCII)) {
396
397 1 1. walkAndLoad : removed call to java/util/stream/Stream::forEach → KILLED
                fileStream.forEach(line -> {
398 1 1. lambda$walkAndLoad$15 : removed call to java/security/MessageDigest::update → KILLED
                    messageDigestSha256.update(line.getBytes(StandardCharsets.US_ASCII));
399 1 1. lambda$walkAndLoad$15 : removed call to com/renomad/minum/database/DbEngine2::readAndDeserialize → KILLED
                    readAndDeserialize(line, fileName);
400
                });
401
402
                // check against the checksum for what we read, if applicable
403 1 1. walkAndLoad : negated conditional → KILLED
                if (fileUtils.exists(checksumFilename)) {
404
                    String checksum = fileUtils.readString(checksumFilename);
405
                    byte[] hashBytes = messageDigestSha256.digest();
406
                    String hashString = CryptoUtils.bytesToHex(hashBytes);
407 1 1. walkAndLoad : negated conditional → KILLED
                    if (!hashString.equals(checksum)) {
408
                        String errorMessage = generateChecksumErrorMessage(consolidatedDataFile);
409
                        throw new DbChecksumException(errorMessage);
410
                    }
411
                }
412
413
            } catch (Exception e) {
414
                throw new DbException(e);
415
            }
416
        }
417
    }
418
419
    /**
420
     * Given a file like 1_to_1000 or 1001_to_2000, extract out the
421
     * beginning index (i.e. 1, or 1001).
422
     */
423
    static long parseConsolidatedFileName(String file) {
424
        int index = file.indexOf("_to_");
425 1 1. parseConsolidatedFileName : negated conditional → TIMED_OUT
        if (index == -1) {
426
            throw new DbException("Consolidated filename was invalid: " + file);
427
        }
428 1 1. parseConsolidatedFileName : replaced long return with 0 for com/renomad/minum/database/DbEngine2::parseConsolidatedFileName → SURVIVED
        return Long.parseLong(file, 0, index, 10);
429
    }
430
431
    /**
432
     * Converts a serialized string to a strongly-typed data structure
433
     * and adds it to the database.
434
     */
435
    void readAndDeserialize(String lineOfData, String fileName) {
436
        try {
437
            @SuppressWarnings("unchecked")
438
            T deserializedData = (T) emptyInstance.deserialize(lineOfData);
439
            mustBeTrue(deserializedData != null, "deserialization of " + emptyInstance +
440
                    " resulted in a null value. Was the serialization method implemented properly?");
441
442
            // put the data into the in-memory data structure
443
            data.put(deserializedData.getIndex(), deserializedData);
444 1 1. readAndDeserialize : removed call to com/renomad/minum/database/DbEngine2::addToIndexes → KILLED
            addToIndexes(deserializedData);
445
446
        } catch (Exception e) {
447
            throw new DbException("Failed to deserialize " + lineOfData + " with data (\"" + fileName + "\"). Caused by: " + e);
448
        }
449
    }
450
451
452
    /**
453
     * This is what loads the data from disk the
454
     * first time someone needs it.  Because it is
455
     * locked, only one thread can enter at
456
     * a time.  The first one in will load the data,
457
     * and the second will encounter a branch which skips loading.
458
     */
459
    @Override
460
    public DbEngine2<T> loadData() {
461 1 1. loadData : removed call to java/util/concurrent/locks/ReentrantLock::lock → TIMED_OUT
        loadDataLock.lock(); // block threads here if multiple are trying to get in - only one gets in at a time
462
        try {
463 1 1. loadData : negated conditional → TIMED_OUT
            if (!hasLoadedData) {
464 1 1. loadData : removed call to com/renomad/minum/database/DbEngine2::loadDataFromDisk → KILLED
                loadDataFromDisk();
465
            }
466
            hasLoadedData = true;
467 1 1. loadData : replaced return value with null for com/renomad/minum/database/DbEngine2::loadData → KILLED
            return this;
468
        } catch (Exception ex) {
469
            throw new DbException("Failed to load data from disk for database with path " + this.dbDirectory, ex);
470
        } finally {
471 1 1. loadData : removed call to java/util/concurrent/locks/ReentrantLock::unlock → KILLED
            loadDataLock.unlock();
472
        }
473
    }
474
475
    /**
476
     * This method provides read capability for the values of a database.
477
     * <br>
478
     * The returned collection is a read-only view over the data, through {@link Collections#unmodifiableCollection(Collection)}
479
     *
480
     * <p><em>Example:</em></p>
481
     * {@snippet :
482
     * boolean doesUserAlreadyExist(String username) {
483
     *     return userDb.values().stream().anyMatch(x -> x.getUsername().equals(username));
484
     * }
485
     * }
486
     */
487
    @Override
488
    public Collection<T> values() {
489
        // load data if needed
490 1 1. values : negated conditional → KILLED
        if (!hasLoadedData) loadData();
491
492 1 1. values : replaced return value with Collections.emptyList for com/renomad/minum/database/DbEngine2::values → KILLED
        return Collections.unmodifiableCollection(data.values());
493
    }
494
495
    @Override
496
    public DbEngine2<T> registerIndex(String indexName, Function<T, String> keyObtainingFunction) {
497 1 1. registerIndex : negated conditional → KILLED
        if (hasLoadedData) {
498
            throw new DbException("This method must be run before the database loads data from disk.  Typically, " +
499
                    "it should be run immediately after the database is created.  See this method's documentation");
500
        }
501
        super.registerIndex(indexName, keyObtainingFunction);
502 1 1. registerIndex : replaced return value with null for com/renomad/minum/database/DbEngine2::registerIndex → KILLED
        return this;
503
    }
504
505
506
    @Override
507
    public Collection<T> getIndexedData(String indexName, String key) {
508
        // load data if needed
509 1 1. getIndexedData : negated conditional → KILLED
        if (!hasLoadedData) loadData();
510 1 1. getIndexedData : replaced return value with Collections.emptyList for com/renomad/minum/database/DbEngine2::getIndexedData → KILLED
        return super.getIndexedData(indexName, key);
511
    }
512
513
    /**
514
     * This is here to match the contract of {@link Db}
515
     * but all it does is tell the interior file writer
516
     * to write its data to disk.
517
     */
518
    @Override
519
    public void stop() throws IOException {
520 1 1. stop : removed call to com/renomad/minum/state/Context::removeFromPaths → KILLED
        context.removeFromPaths(this.dbDirectory);
521 1 1. stop : removed call to com/renomad/minum/database/DatabaseAppender::flush → KILLED
        this.databaseAppender.flush();
522
    }
523
524
    /**
525
     * No real difference to {@link #stop()} but here
526
     * to have a similar contract to {@link Db}
527
     */
528
    @Override
529
    public void stop(int count, long sleepTime) throws IOException {
530 1 1. stop : removed call to com/renomad/minum/database/DbEngine2::stop → KILLED
        this.stop();
531
    }
532
}

Mutations

191

1.1
Location : write
Killed by : com.renomad.minum.web.WebTests
removed call to com/renomad/minum/database/DbEngine2::basicDataChecks → KILLED

194

1.1
Location : write
Killed by : com.renomad.minum.FunctionalTests.testEndToEnd_Functional(com.renomad.minum.FunctionalTests)
negated conditional → KILLED

199

1.1
Location : write
Killed by : com.renomad.minum.web.WebTests
negated conditional → KILLED

204

1.1
Location : write
Killed by : com.renomad.minum.database.DbEngine2Tests
removed call to com/renomad/minum/database/InspectableLock::lock → KILLED

211

1.1
Location : write
Killed by : com.renomad.minum.web.WebTests
negated conditional → KILLED

212

1.1
Location : write
Killed by : none
Replaced long subtraction with addition → SURVIVED
Covering tests

218

1.1
Location : write
Killed by : com.renomad.minum.web.WebTests
removed call to com/renomad/minum/database/DbEngine2::writeToDisk → KILLED

219

1.1
Location : write
Killed by : com.renomad.minum.web.WebTests
removed call to com/renomad/minum/database/DbEngine2::writeToMemory → KILLED

223

1.1
Location : write
Killed by : com.renomad.minum.web.WebTests
Replaced long subtraction with addition → KILLED

225

1.1
Location : write
Killed by : com.renomad.minum.web.WebTests
removed call to com/renomad/minum/database/InspectableLock::unlock → KILLED

230

1.1
Location : write
Killed by : com.renomad.minum.web.WebTests
replaced return value with null for com/renomad/minum/database/DbEngine2::write → KILLED

250

1.1
Location : consolidateIfNecessary
Killed by : com.renomad.minum.web.WebTests
changed conditional boundary → KILLED

2.2
Location : consolidateIfNecessary
Killed by : com.renomad.minum.database.DbEngine2Tests
negated conditional → KILLED

3.3
Location : consolidateIfNecessary
Killed by : com.renomad.minum.security.TheBrigTests
negated conditional → KILLED

251

1.1
Location : consolidateIfNecessary
Killed by : com.renomad.minum.database.DbEngine2Tests
removed call to java/util/concurrent/locks/ReentrantLock::lock → KILLED

253

1.1
Location : consolidateIfNecessary
Killed by : none
removed call to com/renomad/minum/database/DbEngine2::consolidateInnerCode → TIMED_OUT

255

1.1
Location : consolidateIfNecessary
Killed by : none
removed call to java/util/concurrent/locks/ReentrantLock::unlock → SURVIVED
Covering tests

257

1.1
Location : consolidateIfNecessary
Killed by : none
replaced boolean return with false for com/renomad/minum/database/DbEngine2::consolidateIfNecessary → TIMED_OUT

259

1.1
Location : consolidateIfNecessary
Killed by : com.renomad.minum.web.WebTests
replaced boolean return with true for com/renomad/minum/database/DbEngine2::consolidateIfNecessary → KILLED

268

1.1
Location : consolidateInnerCode
Killed by : com.renomad.minum.database.DbEngine2Tests
negated conditional → KILLED

2.2
Location : consolidateInnerCode
Killed by : none
negated conditional → TIMED_OUT

3.3
Location : consolidateInnerCode
Killed by : com.renomad.minum.database.DbEngine2Tests
changed conditional boundary → KILLED

272

1.1
Location : lambda$consolidateInnerCode$6
Killed by : com.renomad.minum.database.DbEngine2Tests
removed call to com/renomad/minum/database/DatabaseConsolidator::consolidate → KILLED

278

1.1
Location : consolidateInnerCode
Killed by : com.renomad.minum.database.DbEngine2Tests
removed call to java/util/concurrent/atomic/AtomicInteger::set → KILLED

293

1.1
Location : delete
Killed by : com.renomad.minum.database.DbEngine2Tests
removed call to com/renomad/minum/database/DbEngine2::basicDataChecks → KILLED

296

1.1
Location : delete
Killed by : none
negated conditional → TIMED_OUT

301

1.1
Location : delete
Killed by : com.renomad.minum.security.TheBrigTests
negated conditional → KILLED

306

1.1
Location : delete
Killed by : none
removed call to com/renomad/minum/database/InspectableLock::lock → SURVIVED
Covering tests

313

1.1
Location : delete
Killed by : none
negated conditional → TIMED_OUT

314

1.1
Location : delete
Killed by : none
Replaced long subtraction with addition → SURVIVED
Covering tests

319

1.1
Location : delete
Killed by : com.renomad.minum.security.TheBrigTests
removed call to com/renomad/minum/database/DbEngine2::deleteFromDisk → KILLED

320

1.1
Location : delete
Killed by : com.renomad.minum.security.TheBrigTests
removed call to com/renomad/minum/database/DbEngine2::deleteFromMemory → KILLED

324

1.1
Location : delete
Killed by : none
Replaced long subtraction with addition → TIMED_OUT

326

1.1
Location : delete
Killed by : none
removed call to com/renomad/minum/database/InspectableLock::unlock → TIMED_OUT

343

1.1
Location : loadDataFromDisk
Killed by : com.renomad.minum.web.WebPerformanceTests.webPerfTest(com.renomad.minum.web.WebPerformanceTests)
negated conditional → KILLED

344

1.1
Location : loadDataFromDisk
Killed by : com.renomad.minum.database.DbEngine2Tests
removed call to com/renomad/minum/database/DbFileConverter::convertClassicFolderStructureToDbEngine2Form → KILLED

350

1.1
Location : loadDataFromDisk
Killed by : com.renomad.minum.web.WebPerformanceTests.webPerfTest(com.renomad.minum.web.WebPerformanceTests)
removed call to com/renomad/minum/database/DatabaseAppender::flush → KILLED

353

1.1
Location : loadDataFromDisk
Killed by : com.renomad.minum.web.WebPerformanceTests.webPerfTest(com.renomad.minum.web.WebPerformanceTests)
removed call to com/renomad/minum/database/DatabaseConsolidator::consolidate → KILLED

356

1.1
Location : loadDataFromDisk
Killed by : com.renomad.minum.web.WebPerformanceTests.webPerfTest(com.renomad.minum.web.WebPerformanceTests)
removed call to com/renomad/minum/database/DbEngine2::walkAndLoad → KILLED

358

1.1
Location : loadDataFromDisk
Killed by : com.renomad.minum.web.WebPerformanceTests.webPerfTest(com.renomad.minum.web.WebPerformanceTests)
negated conditional → KILLED

361

1.1
Location : loadDataFromDisk
Killed by : com.renomad.minum.security.TheBrigTests
Replaced long addition with subtraction → KILLED

374

1.1
Location : lambda$walkAndLoad$13
Killed by : com.renomad.minum.security.TheBrigTests
replaced boolean return with true for com/renomad/minum/database/DbEngine2::lambda$walkAndLoad$13 → KILLED

2.2
Location : lambda$walkAndLoad$13
Killed by : com.renomad.minum.security.TheBrigTests
negated conditional → KILLED

378

1.1
Location : walkAndLoad
Killed by : com.renomad.minum.FunctionalTests.test_EdgeCase_BadRequest2(com.renomad.minum.FunctionalTests)
negated conditional → KILLED

381

1.1
Location : walkAndLoad
Killed by : none
removed call to java/util/List::sort → TIMED_OUT

397

1.1
Location : walkAndLoad
Killed by : com.renomad.minum.security.TheBrigTests
removed call to java/util/stream/Stream::forEach → KILLED

398

1.1
Location : lambda$walkAndLoad$15
Killed by : com.renomad.minum.security.TheBrigTests
removed call to java/security/MessageDigest::update → KILLED

399

1.1
Location : lambda$walkAndLoad$15
Killed by : com.renomad.minum.security.TheBrigTests
removed call to com/renomad/minum/database/DbEngine2::readAndDeserialize → KILLED

403

1.1
Location : walkAndLoad
Killed by : com.renomad.minum.security.TheBrigTests
negated conditional → KILLED

407

1.1
Location : walkAndLoad
Killed by : com.renomad.minum.security.TheBrigTests
negated conditional → KILLED

425

1.1
Location : parseConsolidatedFileName
Killed by : none
negated conditional → TIMED_OUT

428

1.1
Location : parseConsolidatedFileName
Killed by : none
replaced long return with 0 for com/renomad/minum/database/DbEngine2::parseConsolidatedFileName → SURVIVED
Covering tests

444

1.1
Location : readAndDeserialize
Killed by : com.renomad.minum.security.TheBrigTests
removed call to com/renomad/minum/database/DbEngine2::addToIndexes → KILLED

461

1.1
Location : loadData
Killed by : none
removed call to java/util/concurrent/locks/ReentrantLock::lock → TIMED_OUT

463

1.1
Location : loadData
Killed by : none
negated conditional → TIMED_OUT

464

1.1
Location : loadData
Killed by : com.renomad.minum.web.WebPerformanceTests.webPerfTest(com.renomad.minum.web.WebPerformanceTests)
removed call to com/renomad/minum/database/DbEngine2::loadDataFromDisk → KILLED

467

1.1
Location : loadData
Killed by : com.renomad.minum.web.WebPerformanceTests.webPerfTest(com.renomad.minum.web.WebPerformanceTests)
replaced return value with null for com/renomad/minum/database/DbEngine2::loadData → KILLED

471

1.1
Location : loadData
Killed by : com.renomad.minum.web.WebPerformanceTests.webPerfTest(com.renomad.minum.web.WebPerformanceTests)
removed call to java/util/concurrent/locks/ReentrantLock::unlock → KILLED

490

1.1
Location : values
Killed by : com.renomad.minum.FunctionalTests.test_PathFunction_Response(com.renomad.minum.FunctionalTests)
negated conditional → KILLED

492

1.1
Location : values
Killed by : com.renomad.minum.web.WebPerformanceTests.webPerfTest(com.renomad.minum.web.WebPerformanceTests)
replaced return value with Collections.emptyList for com/renomad/minum/database/DbEngine2::values → KILLED

497

1.1
Location : registerIndex
Killed by : com.renomad.minum.web.WebPerformanceTests.webPerfTest(com.renomad.minum.web.WebPerformanceTests)
negated conditional → KILLED

502

1.1
Location : registerIndex
Killed by : com.renomad.minum.web.WebPerformanceTests.webPerfTest(com.renomad.minum.web.WebPerformanceTests)
replaced return value with null for com/renomad/minum/database/DbEngine2::registerIndex → KILLED

509

1.1
Location : getIndexedData
Killed by : com.renomad.minum.FunctionalTests.test_PathFunction_Response(com.renomad.minum.FunctionalTests)
negated conditional → KILLED

510

1.1
Location : getIndexedData
Killed by : com.renomad.minum.FunctionalTests.test_PathFunction_Response(com.renomad.minum.FunctionalTests)
replaced return value with Collections.emptyList for com/renomad/minum/database/DbEngine2::getIndexedData → KILLED

520

1.1
Location : stop
Killed by : com.renomad.minum.security.TheBrigTests
removed call to com/renomad/minum/state/Context::removeFromPaths → KILLED

521

1.1
Location : stop
Killed by : com.renomad.minum.database.DbEngine2Tests
removed call to com/renomad/minum/database/DatabaseAppender::flush → KILLED

530

1.1
Location : stop
Killed by : com.renomad.minum.database.DbEngine2Tests
removed call to com/renomad/minum/database/DbEngine2::stop → KILLED

Active mutators

Tests examined


Report generated by PIT 1.17.0