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.util.*;
13
import java.util.concurrent.ConcurrentHashMap;
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
     * Setting to volatile since it is used as a flag, and changes from multiple
127
     * threads means we want the freshest status possible for any caller.
128
     */
129
    volatile boolean consolidationIsRunning;
130
131
    /**
132
     * Constructs an in-memory disk-persisted database.
133
     * Loading of data from disk happens at the first invocation of any command
134
     * changing or requesting data, such as {@link #write(DbData)}, {@link #delete(DbData)},
135
     * or {@link #values()}.  See the private method loadData() for details.
136
     * @param dbDirectory this uniquely names your database, and also sets the directory
137
     *                    name for this data.  The expected use case is to name this after
138
     *                    the data in question.  For example, "users", or "accounts".
139
     * @param context used to provide important state data to several components
140
     * @param instance an instance of the {@link DbData} object relevant for use in this database. Note
141
     *                 that each database (that is, each instance of this class), focuses on just one
142
     *                 data, which must be an implementation of {@link DbData}.
143
     */
144
    public DbEngine2(Path dbDirectory, Context context, T instance) {
145
        this(dbDirectory, context, instance, new FileUtils(context.getLogger(), context.getConstants()));
146
    }
147
148
    DbEngine2(Path dbDirectory, Context context, T instance, IFileUtils fileUtils) {
149
        super(dbDirectory, context, instance, fileUtils);
150
151
        try {
152
            this.databaseConsolidator = new DatabaseConsolidator(dbDirectory, context, fileUtils);
153
            this.databaseAppender = new DatabaseAppender(dbDirectory, context, fileUtils);
154
        } catch (IOException ex) {
155
            throw new DbException("Error in DbEngine2 constructor", ex);
156
        }
157
        this.loadDataLock = new ReentrantLock();
158
        this.consolidateLock = new ReentrantLock();
159
        this.maxLinesPerAppendFile = context.getConstants().maxAppendCount;
160
    }
161
162
    /**
163
     * Write data to the database.  Use an index of 0 to store new data, and a positive
164
     * non-zero value to update data.
165
     * <p><em>
166
     *     Example of adding new data to the database:
167
     * </em></p>
168
     * {@snippet :
169
     *          final var newSalt = StringUtils.generateSecureRandomString(10);
170
     *          final var hashedPassword = CryptoUtils.createPasswordHash(newPassword, newSalt);
171
     *          final var newUser = new User(0L, newUsername, hashedPassword, newSalt);
172
     *          userDb.write(newUser);
173
     * }
174
     * <p><em>
175
     *     Example of updating data:
176
     * </em></p>
177
     * {@snippet :
178
     *         // write the updated salted password to the database
179
     *         final var updatedUser = new User(
180
     *                 user().getIndex(),
181
     *                 user().getUsername(),
182
     *                 hashedPassword,
183
     *                 newSalt);
184
     *         userDb.write(updatedUser);
185
     * }
186
     *
187
     * @param newData the data we are writing
188
     * @return the data with its new index assigned.
189
     * @throws DbException if there is a failure to write
190
     */
191
    @Override
192
    public T write(T newData) {
193 1 1. write : removed call to com/renomad/minum/database/DbEngine2::basicDataChecks → KILLED
        basicDataChecks(newData);
194
195
        // load data if needed
196 1 1. write : negated conditional → KILLED
        if (!hasLoadedData) loadData();
197
198
        boolean hasBeenBLocked;
199
        long startWaitingTime;
200
        long threadId = Thread.currentThread().threadId();
201 1 1. write : negated conditional → KILLED
        if (!dbLock.tryLock()) {
202
            Thread lockOwner = dbLock.getLockOwner();
203
            logger.logTrace(() -> "Thread %d encountered a lock held by Thread %s during write()".formatted(threadId, getLockOwnerIdString(lockOwner)));
204
            hasBeenBLocked = true;
205
            startWaitingTime = System.currentTimeMillis();
206 1 1. write : removed call to com/renomad/minum/database/InspectableLock::lock → TIMED_OUT
            dbLock.lock();
207
        } else {
208
            hasBeenBLocked = false;
209
            startWaitingTime = 0;
210
            logger.logTrace(() -> "Thread %d has acquired the dbLock lock for DbEngine2.write()".formatted(threadId));
211
        }
212
213 1 1. write : negated conditional → TIMED_OUT
        if (hasBeenBLocked) {
214 1 1. write : Replaced long subtraction with addition → TIMED_OUT
            long waitTimeMillis = System.currentTimeMillis() - startWaitingTime;
215
            logger.logTrace(() -> "Thread %d successfully acquired the lock after waiting %d milliseconds".formatted(threadId, waitTimeMillis));
216
        }
217
        long startProcessingTime = System.currentTimeMillis();
218
        try {
219
            boolean newElementCreated = processDataIndex(newData);
220 1 1. write : removed call to com/renomad/minum/database/DbEngine2::writeToDisk → KILLED
            writeToDisk(newData);
221 1 1. write : removed call to com/renomad/minum/database/DbEngine2::writeToMemory → KILLED
            writeToMemory(newData, newElementCreated);
222
        } catch (Exception ex) {
223
           throw new DbException("failed to write data " + newData, ex);
224
        } finally {
225 1 1. write : Replaced long subtraction with addition → KILLED
            long processingTime = System.currentTimeMillis() - startProcessingTime;
226
            logger.logTrace(() -> "Thread %d releasing lock for writing.  Time taken in millis: %d".formatted(Thread.currentThread().threadId(), processingTime));
227 1 1. write : removed call to com/renomad/minum/database/InspectableLock::unlock → KILLED
            dbLock.unlock();
228
        }
229
230
        // returning the data at this point is the most convenient
231
        // way users will have access to the new index of the data.
232 1 1. write : replaced return value with null for com/renomad/minum/database/DbEngine2::write → KILLED
        return newData;
233
    }
234
235
    private void writeToDisk(T newData) throws IOException {
236
        logger.logTrace(() -> String.format("Thread %d is writing data to disk: %s", Thread.currentThread().threadId(), newData));
237
        String serializedData = newData.serialize();
238
        mustBeFalse(serializedData == null || serializedData.isBlank(),
239
                "the serialized form of data must not be blank. " +
240
                        "Is the serialization code written properly? Our datatype: " + emptyInstance);
241
        databaseAppender.appendToDatabase(DatabaseChangeAction.UPDATE, serializedData);
242
        appendCount.incrementAndGet();
243
        consolidateIfNecessary();
244
    }
245
246
    /**
247
     * If the append count is large enough, we will call the
248
     * consolidation method on the DatabaseConsolidator and
249
     * reset the append count to 0.
250
     */
251
    boolean consolidateIfNecessary() {
252 3 1. consolidateIfNecessary : negated conditional → TIMED_OUT
2. consolidateIfNecessary : changed conditional boundary → KILLED
3. consolidateIfNecessary : negated conditional → KILLED
        if (appendCount.get() > maxLinesPerAppendFile && !consolidationIsRunning) {
253 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
254
            try {
255 1 1. consolidateIfNecessary : removed call to com/renomad/minum/database/DbEngine2::consolidateInnerCode → TIMED_OUT
                consolidateInnerCode();
256
            } finally {
257 1 1. consolidateIfNecessary : removed call to java/util/concurrent/locks/ReentrantLock::unlock → TIMED_OUT
                consolidateLock.unlock();
258
            }
259 1 1. consolidateIfNecessary : replaced boolean return with false for com/renomad/minum/database/DbEngine2::consolidateIfNecessary → TIMED_OUT
            return true;
260
        }
261 1 1. consolidateIfNecessary : replaced boolean return with true for com/renomad/minum/database/DbEngine2::consolidateIfNecessary → KILLED
        return false;
262
    }
263
264
    /**
265
     * This code is only called in production from {@link #consolidateIfNecessary()},
266
     * and is necessarily protected by mutex locks.  However, it is provided
267
     * here as its own method for ease of testing.
268
     */
269
    void consolidateInnerCode() {
270 3 1. consolidateInnerCode : negated conditional → TIMED_OUT
2. consolidateInnerCode : changed conditional boundary → TIMED_OUT
3. consolidateInnerCode : negated conditional → KILLED
        if (appendCount.get() > maxLinesPerAppendFile && !consolidationIsRunning) {
271
            context.getExecutorService().submit(() -> {
272
                try {
273
                    consolidationIsRunning = true;
274 1 1. lambda$consolidateInnerCode$6 : removed call to com/renomad/minum/database/DatabaseConsolidator::consolidate → TIMED_OUT
                    databaseConsolidator.consolidate();
275
                } catch (Exception e) {
276
                    logger.logAsyncError(() -> "Error during consolidation: " + e);
277
                } finally {
278
                    consolidationIsRunning = false;
279
                }
280
            });
281 1 1. consolidateInnerCode : removed call to java/util/concurrent/atomic/AtomicInteger::set → TIMED_OUT
            appendCount.set(0);
282
        }
283
    }
284
285
    /**
286
     * Delete data
287
     * <p><em>Example:</em></p>
288
     * {@snippet :
289
     *      userDb.delete(user);
290
     * }
291
     * @param dataToDelete the data we are serializing and writing
292
     * @throws DbException if there is a failure to delete
293
     */
294
    @Override
295
    public void delete(T dataToDelete) {
296 1 1. delete : removed call to com/renomad/minum/database/DbEngine2::basicDataChecks → TIMED_OUT
        basicDataChecks(dataToDelete);
297
298
        // load data if needed
299 1 1. delete : negated conditional → TIMED_OUT
        if (!hasLoadedData) loadData();
300
301
        boolean hasBeenBLocked;
302
        long startWaitingTime;
303
        long threadId = Thread.currentThread().threadId();
304 1 1. delete : negated conditional → TIMED_OUT
        if (!dbLock.tryLock()) {
305
            Thread lockOwner = dbLock.getLockOwner();
306
            logger.logTrace(() -> "Thread %d encountered a lock held by Thread %s during delete()".formatted(threadId, getLockOwnerIdString(lockOwner)));
307
            hasBeenBLocked = true;
308
            startWaitingTime = System.currentTimeMillis();
309 1 1. delete : removed call to com/renomad/minum/database/InspectableLock::lock → TIMED_OUT
            dbLock.lock();
310
        } else {
311
            hasBeenBLocked = false;
312
            startWaitingTime = 0;
313
            logger.logTrace(() -> "Thread %d has acquired the dbLock lock for DbEngine2.delete()".formatted(threadId));
314
        }
315
316 1 1. delete : negated conditional → TIMED_OUT
        if (hasBeenBLocked) {
317 1 1. delete : Replaced long subtraction with addition → TIMED_OUT
            long waitTimeMillis = System.currentTimeMillis() - startWaitingTime;
318
            logger.logTrace(() -> "Thread %d successfully acquired the lock after waiting %d milliseconds".formatted(threadId, waitTimeMillis));
319
        }
320
        long startProcessingTime = System.currentTimeMillis();
321
        try {
322 1 1. delete : removed call to com/renomad/minum/database/DbEngine2::deleteFromDisk → KILLED
            deleteFromDisk(dataToDelete);
323 1 1. delete : removed call to com/renomad/minum/database/DbEngine2::deleteFromMemory → TIMED_OUT
            deleteFromMemory(dataToDelete);
324
        } catch (Exception ex) {
325
            throw new DbException("failed to delete data " + dataToDelete, ex);
326
        } finally {
327 1 1. delete : Replaced long subtraction with addition → TIMED_OUT
            long processingTime = System.currentTimeMillis() - startProcessingTime;
328
            logger.logTrace(() -> "Thread %d releasing lock for writing.  Time taken in millis: %d".formatted(Thread.currentThread().threadId(), processingTime));
329 1 1. delete : removed call to com/renomad/minum/database/InspectableLock::unlock → TIMED_OUT
            dbLock.unlock();
330
        }
331
    }
332
333
    private void deleteFromDisk(T dataToDelete) throws IOException {
334
        logger.logTrace(() -> String.format("Thread %d deleting data from disk: %s", Thread.currentThread().threadId(), dataToDelete));
335
        databaseAppender.appendToDatabase(DatabaseChangeAction.DELETE, dataToDelete.serialize());
336
        appendCount.incrementAndGet();
337
        consolidateIfNecessary();
338
    }
339
340
    private void loadDataFromDisk() throws IOException {
341
        logger.logDebug(() -> "Loading data from disk. Db Engine2. Directory: " + dbDirectory);
342
343
        // if we find the "index.ddps" file, it means we are looking at an old
344
        // version of the database.  Update it to the new version, and then afterwards
345
        // remove the old version files.
346 1 1. loadDataFromDisk : negated conditional → KILLED
        if (fileUtils.exists(dbDirectory.resolve("index.ddps"))) {
347 1 1. loadDataFromDisk : removed call to com/renomad/minum/database/DbFileConverter::convertClassicFolderStructureToDbEngine2Form → TIMED_OUT
            new DbFileConverter(context, dbDirectory, fileUtils).convertClassicFolderStructureToDbEngine2Form();
348
        }
349
350
        // if there are any remaining items in the current append-only file, move them
351
        // to a new file
352
        databaseAppender.saveOffCurrentDataToReadyFolder();
353 1 1. loadDataFromDisk : removed call to com/renomad/minum/database/DatabaseAppender::flush → TIMED_OUT
        databaseAppender.flush();
354
355
        // consolidate whatever files still exist in the append logs
356 1 1. loadDataFromDisk : removed call to com/renomad/minum/database/DatabaseConsolidator::consolidate → TIMED_OUT
        databaseConsolidator.consolidate();
357
358
        // load the data into memory
359 1 1. loadDataFromDisk : removed call to com/renomad/minum/database/DbEngine2::walkAndLoad → TIMED_OUT
        walkAndLoad(dbDirectory);
360
361 1 1. loadDataFromDisk : negated conditional → KILLED
        if (data.isEmpty()) {
362
            this.index = new AtomicLong(1);
363
        } else {
364 1 1. loadDataFromDisk : Replaced long addition with subtraction → KILLED
            var initialIndex = Collections.max(data.keySet()) + 1L;
365
            this.index = new AtomicLong(initialIndex);
366
        }
367
    }
368
369
    /**
370
     * Loops through each line of data in the consolidated data files,
371
     * converting each to its strongly-typed form and adding to the database
372
     */
373
    void walkAndLoad(Path dbDirectory) {
374
        List<String> consolidatedFiles = new ArrayList<>(
375
                Arrays.stream(Objects.requireNonNull(
376
                        dbDirectory.resolve("consolidated_data").toFile().list()))
377 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"))
378
                        .toList());
379
380
        // if there aren't any files, bail out
381 1 1. walkAndLoad : negated conditional → TIMED_OUT
        if (consolidatedFiles.isEmpty()) return;
382
383
        // sort
384 1 1. walkAndLoad : removed call to java/util/List::sort → TIMED_OUT
        consolidatedFiles.sort(Comparator.comparingLong(DbEngine2::parseConsolidatedFileName));
385
386
        for (String fileName : consolidatedFiles) {
387
            logger.logDebug(() -> "Processing database file: " + fileName);
388
            Path consolidatedDataFile = dbDirectory.resolve("consolidated_data").resolve(fileName);
389
            Path checksumFilename = consolidatedDataFile.resolveSibling(consolidatedDataFile.getFileName() + ".checksum");
390
391
392
            // By using a lazy stream, we are able to read each item from the file into
393
            // memory without needing to read the whole file contents into memory at once,
394
            // thus avoiding requiring a great amount of memory
395
            // build a hash for this data
396
            MessageDigest messageDigestSha256 = getMessageDigest("SHA-256");
397
398
            try(Stream<String> fileStream = fileUtils.lines(consolidatedDataFile, StandardCharsets.US_ASCII)) {
399
400 1 1. walkAndLoad : removed call to java/util/stream/Stream::forEach → TIMED_OUT
                fileStream.forEach(line -> {
401 1 1. lambda$walkAndLoad$15 : removed call to java/security/MessageDigest::update → KILLED
                    messageDigestSha256.update(line.getBytes(StandardCharsets.US_ASCII));
402 1 1. lambda$walkAndLoad$15 : removed call to com/renomad/minum/database/DbEngine2::readAndDeserialize → KILLED
                    readAndDeserialize(line, fileName);
403
                });
404
405
                // check against the checksum for what we read, if applicable
406 1 1. walkAndLoad : negated conditional → TIMED_OUT
                if (fileUtils.exists(checksumFilename)) {
407
                    String checksum = fileUtils.readString(checksumFilename);
408
                    byte[] hashBytes = messageDigestSha256.digest();
409
                    String hashString = CryptoUtils.bytesToHex(hashBytes);
410 1 1. walkAndLoad : negated conditional → KILLED
                    if (!hashString.equals(checksum)) {
411
                        String errorMessage = generateChecksumErrorMessage(consolidatedDataFile);
412
                        throw new DbChecksumException(errorMessage);
413
                    }
414
                }
415
416
            } catch (Exception e) {
417
                throw new DbException(e);
418
            }
419
        }
420
    }
421
422
    /**
423
     * Given a file like 1_to_1000 or 1001_to_2000, extract out the
424
     * beginning index (i.e. 1, or 1001).
425
     */
426
    static long parseConsolidatedFileName(String file) {
427
        int index = file.indexOf("_to_");
428 1 1. parseConsolidatedFileName : negated conditional → TIMED_OUT
        if (index == -1) {
429
            throw new DbException("Consolidated filename was invalid: " + file);
430
        }
431 1 1. parseConsolidatedFileName : replaced long return with 0 for com/renomad/minum/database/DbEngine2::parseConsolidatedFileName → TIMED_OUT
        return Long.parseLong(file, 0, index, 10);
432
    }
433
434
    /**
435
     * Converts a serialized string to a strongly-typed data structure
436
     * and adds it to the database.
437
     */
438
    void readAndDeserialize(String lineOfData, String fileName) {
439
        try {
440
            @SuppressWarnings("unchecked")
441
            T deserializedData = (T) emptyInstance.deserialize(lineOfData);
442
            mustBeTrue(deserializedData != null, "deserialization of " + emptyInstance +
443
                    " resulted in a null value. Was the serialization method implemented properly?");
444
445
            // put the data into the in-memory data structure
446
            data.put(deserializedData.getIndex(), deserializedData);
447 1 1. readAndDeserialize : removed call to com/renomad/minum/database/DbEngine2::addToIndexes → TIMED_OUT
            addToIndexes(deserializedData);
448
449
        } catch (Exception e) {
450
            throw new DbException("Failed to deserialize " + lineOfData + " with data (\"" + fileName + "\"). Caused by: " + e);
451
        }
452
    }
453
454
455
    /**
456
     * This is what loads the data from disk the
457
     * first time someone needs it.  Because it is
458
     * locked, only one thread can enter at
459
     * a time.  The first one in will load the data,
460
     * and the second will encounter a branch which skips loading.
461
     */
462
    @Override
463
    public DbEngine2<T> loadData() {
464 1 1. loadData : removed call to java/util/concurrent/locks/ReentrantLock::lock → KILLED
        loadDataLock.lock(); // block threads here if multiple are trying to get in - only one gets in at a time
465
        try {
466 1 1. loadData : negated conditional → TIMED_OUT
            if (!hasLoadedData) {
467
                // the following statements get the database's state initialized,
468
                // so in the edge case that the data load fails, then when it is
469
                // tried again, the containers will be clean and ready.
470
471
                // Initialize the database to an empty map
472
                this.data = new ConcurrentHashMap<>();
473
474
                // Initialize each of the registered indexes to a new empty map
475 2 1. lambda$loadData$16 : replaced return value with Collections.emptyMap for com/renomad/minum/database/DbEngine2::lambda$loadData$16 → TIMED_OUT
2. loadData : removed call to java/util/Map::replaceAll → TIMED_OUT
                registeredIndexes.replaceAll((key, value) -> new HashMap<>());
476
477
                // now actually load the data from disk
478 1 1. loadData : removed call to com/renomad/minum/database/DbEngine2::loadDataFromDisk → TIMED_OUT
                loadDataFromDisk();
479
            }
480
            hasLoadedData = true;
481 1 1. loadData : replaced return value with null for com/renomad/minum/database/DbEngine2::loadData → KILLED
            return this;
482
        } catch (Exception ex) {
483
            throw new DbException("Failed to load data from disk for database with path " + this.dbDirectory, ex);
484
        } finally {
485 1 1. loadData : removed call to java/util/concurrent/locks/ReentrantLock::unlock → TIMED_OUT
            loadDataLock.unlock();
486
        }
487
    }
488
489
    /**
490
     * This method provides read capability for the values of a database.
491
     * <br>
492
     * The returned collection is a read-only view over the data, through {@link Collections#unmodifiableCollection(Collection)}
493
     *
494
     * <p><em>Example:</em></p>
495
     * {@snippet :
496
     * boolean doesUserAlreadyExist(String username) {
497
     *     return userDb.values().stream().anyMatch(x -> x.getUsername().equals(username));
498
     * }
499
     * }
500
     */
501
    @Override
502
    public Collection<T> values() {
503
        // load data if needed
504 1 1. values : negated conditional → KILLED
        if (!hasLoadedData) loadData();
505
506 1 1. values : replaced return value with Collections.emptyList for com/renomad/minum/database/DbEngine2::values → TIMED_OUT
        return Collections.unmodifiableCollection(data.values());
507
    }
508
509
    @Override
510
    public DbEngine2<T> registerIndex(String indexName, Function<T, String> keyObtainingFunction) {
511 1 1. registerIndex : negated conditional → KILLED
        if (hasLoadedData) {
512
            throw new DbException("This method must be run before the database loads data from disk.  Typically, " +
513
                    "it should be run immediately after the database is created.  See this method's documentation");
514
        }
515
        super.registerIndex(indexName, keyObtainingFunction);
516 1 1. registerIndex : replaced return value with null for com/renomad/minum/database/DbEngine2::registerIndex → KILLED
        return this;
517
    }
518
519
520
    @Override
521
    public Collection<T> getIndexedData(String indexName, String key) {
522
        // load data if needed
523 1 1. getIndexedData : negated conditional → TIMED_OUT
        if (!hasLoadedData) loadData();
524 1 1. getIndexedData : replaced return value with Collections.emptyList for com/renomad/minum/database/DbEngine2::getIndexedData → TIMED_OUT
        return super.getIndexedData(indexName, key);
525
    }
526
527
    /**
528
     * This is here to match the contract of {@link Db}
529
     * but all it does is tell the interior file writer
530
     * to write its data to disk.
531
     */
532
    @Override
533
    public void stop() throws IOException {
534 1 1. stop : removed call to com/renomad/minum/state/Context::removeFromPaths → TIMED_OUT
        context.removeFromPaths(this.dbDirectory);
535 1 1. stop : removed call to com/renomad/minum/database/DatabaseAppender::flush → KILLED
        this.databaseAppender.flush();
536
    }
537
538
    /**
539
     * No real difference to {@link #stop()} but here
540
     * to have a similar contract to {@link Db}
541
     */
542
    @Override
543
    public void stop(int count, long sleepTime) throws IOException {
544 1 1. stop : removed call to com/renomad/minum/database/DbEngine2::stop → TIMED_OUT
        this.stop();
545
    }
546
}

Mutations

193

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

196

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

201

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

206

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

213

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

214

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

220

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

221

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

225

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

227

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

232

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

252

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

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

3.3
Location : consolidateIfNecessary
Killed by : com.renomad.minum.web.WebTests
negated conditional → KILLED

253

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

255

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

257

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

259

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

261

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

270

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

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

3.3
Location : consolidateInnerCode
Killed by : none
changed conditional boundary → TIMED_OUT

274

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

281

1.1
Location : consolidateInnerCode
Killed by : none
removed call to java/util/concurrent/atomic/AtomicInteger::set → TIMED_OUT

296

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

299

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

304

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

309

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

316

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

317

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

322

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

323

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

327

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

329

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

346

1.1
Location : loadDataFromDisk
Killed by : com.renomad.minum.web.FindingsWebTests
negated conditional → KILLED

347

1.1
Location : loadDataFromDisk
Killed by : none
removed call to com/renomad/minum/database/DbFileConverter::convertClassicFolderStructureToDbEngine2Form → TIMED_OUT

353

1.1
Location : loadDataFromDisk
Killed by : none
removed call to com/renomad/minum/database/DatabaseAppender::flush → TIMED_OUT

356

1.1
Location : loadDataFromDisk
Killed by : none
removed call to com/renomad/minum/database/DatabaseConsolidator::consolidate → TIMED_OUT

359

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

361

1.1
Location : loadDataFromDisk
Killed by : com.renomad.minum.web.FindingsWebTests
negated conditional → KILLED

364

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

377

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

381

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

384

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

400

1.1
Location : walkAndLoad
Killed by : none
removed call to java/util/stream/Stream::forEach → TIMED_OUT

401

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

402

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

406

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

410

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

428

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

431

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

447

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

464

1.1
Location : loadData
Killed by : com.renomad.minum.web.FindingsWebTests
removed call to java/util/concurrent/locks/ReentrantLock::lock → KILLED

466

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

475

1.1
Location : lambda$loadData$16
Killed by : none
replaced return value with Collections.emptyMap for com/renomad/minum/database/DbEngine2::lambda$loadData$16 → TIMED_OUT

2.2
Location : loadData
Killed by : none
removed call to java/util/Map::replaceAll → TIMED_OUT

478

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

481

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

485

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

504

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

506

1.1
Location : values
Killed by : none
replaced return value with Collections.emptyList for com/renomad/minum/database/DbEngine2::values → TIMED_OUT

511

1.1
Location : registerIndex
Killed by : com.renomad.minum.web.FindingsWebTests
negated conditional → KILLED

516

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

523

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

524

1.1
Location : getIndexedData
Killed by : none
replaced return value with Collections.emptyList for com/renomad/minum/database/DbEngine2::getIndexedData → TIMED_OUT

534

1.1
Location : stop
Killed by : none
removed call to com/renomad/minum/state/Context::removeFromPaths → TIMED_OUT

535

1.1
Location : stop
Killed by : com.renomad.minum.FunctionalTests.test_EdgeCase_Response_MultiCookies(com.renomad.minum.FunctionalTests)
removed call to com/renomad/minum/database/DatabaseAppender::flush → KILLED

544

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

Active mutators

Tests examined


Report generated by PIT 1.17.0