Skip to content

Practice Lab 26 — Failure-safe import with the strong guarantee

Difficulty: Advanced · Estimated time: 6 hours · Module 13: Exception Handling and Robust OOP

Read a bad file, fail, and leave nothing behind. This lab is about the promise an operation makes when it fails — and about proving that promise rather than believing it.


At a glance

Difficulty Advanced
Estimated time 6 hours
Module Module 13 — Exception Handling and Robust OOP
Concepts practised The three exception-safety guarantees, transaction-style operations, RAII under stress, validation at the boundary, failure reporting with context
Prerequisite Practice Lab 25
Class design yours — the guarantees are the contract

Scenario

A clinic imports a patient file exported by another system. The file has patients, their appointments and their notes. The import must satisfy one promise, from the clinic's IT policy:

A failed import changes nothing. The clinic's records are exactly as they were before the attempt, and the operator sees a message saying which record failed and why.

Three files arrive, all of which fail part way through: one truncated, one with an invalid field, and one that is valid except for a duplicate identifier in the middle. The import must fail cleanly for all three — and not fail for the fourth file, which is valid and large.


Problem statement

Build an importer with the strong guarantee.

  1. A patient database holding patients, appointments and notes, with its own invariants (unique patient identifiers, appointments belonging to existing patients, notes attached to appointments).
  2. An importer that reads a documented text format and either applies everything or nothing.
  3. Context in every failure: the line number, the record kind, and the value that failed — carried in an exception whose type tells the caller the kind of failure.
  4. A demonstration of all three guarantees, in three versions of the same operation:
  5. the basic guarantee (no crash, no leak, but a partial import is visible),
  6. the strong guarantee (all or nothing),
  7. the no-throw guarantee for the final commit step, which must be noexcept and demonstrably unable to fail.
  8. A before/after proof: for each failing file, print the counts of patients, appointments and notes before and after the attempt; they must be identical.
  9. A crash-free boundary: the file is untrusted input, so a truncated line, a missing field, a non-numeric identifier and an oversized count are all refused with messages — never a crash, never an allocation based on an unvalidated count.
  10. No leak on any path: the resource counts at exit are zero.

In guarantees.md:

  • The three versions' behaviours, in a table: what a caller observes after a failure in each.
  • Why the basic version is not acceptable for this clinic, in one sentence of consequence.
  • The commit step, and why noexcept is truthful there (what could not fail).
  • The cost of the strong guarantee: what extra work the operation does, and what that costs for the successful large file — a measured comparison against the basic version.

Learning objectives

By the end of this lab you will be able to:

  • State the three exception-safety guarantees and give an example of each from your own code.
  • Build a multi-step operation that either completes or leaves the target unchanged.
  • Identify the commit step that cannot fail, and mark it noexcept honestly.
  • Reject untrusted input with contextual messages rather than crashes.
  • Measure the cost of a strong guarantee against a weaker one.

Functional requirements

  1. The database's invariants hold after every operation, including after a failed import.
  2. A failed import leaves all three collections unchanged — counts identical, and their contents compared, not only counted.
  3. Success on the valid file imports every record, in file order, with a summary printed.
  4. Every refusal names: the line number, the record kind, and the offending value.
  5. A truncated file, a missing field, a non-numeric field and a count over the documented limit are each refused with the right message and no crash.
  6. No allocation is made from an unvalidated count in the file.
  7. The commit step is noexcept and does not allocate.
  8. The basic version's partial import is visible in its counts — that is what the comparison is for.
  9. The large valid file imports in one pass, and its timing or operation count is recorded for the comparison.
  10. The final resource counts are zero, and no exception escapes main.

Constraints

  • Header and source per type family, with a documented build command.
  • Three versions of the import must all exist in the program, clearly labelled, so that the guarantees can be compared rather than described.
  • The imported state is built in a separate container or object and assigned or swapped in at the end — that is the mechanism available at this stage of the course. Say in the notes what the assignment costs, and what would be cheaper.
  • No warnings.

Starter task

In guarantees.md, before coding:

  1. Write the three guarantees with an example of each from your design — including the one you will implement badly on purpose.
  2. Identify the commit step and write what could not fail about it.
  3. Write the failure-context structure you will carry in the exception, and the four fields it needs.
  4. Predict the counts before and after for each of the three bad files, and for the good one.

Implementation tasks

  1. Implement the database and its invariants.
  2. Implement the textual format's parser with full validation, including a count check.
  3. Implement the basic version and record its partial import.
  4. Implement the strong version with the separate staging object and the noexcept commit.
  5. Write the four test files (three bad, one good) as data in the program or as strings; run all four through both versions.
  6. Compare the counts and the operation counts, and record both.
  7. Run the failing paths with the resource counter visible, and confirm zero at exit.

Test cases

# Action Expected result
1 Import the valid file All records; summary printed
2 Import the truncated file (strong) Refused with a line number; counts unchanged
3 Import the invalid-field file (strong) Refused with the field value; counts unchanged
4 Import the duplicate-identifier file (strong) Refused at the duplicate's line; counts unchanged
5 The same three files against the basic version Partial import visible; counts differ — recorded
6 Compare contents, not only counts, after a failed strong import Identical
7 A non-numeric identifier Refused, no crash
8 A count field over the limit Refused before allocating
9 The noexcept commit step Compiles; no allocation inside; documented
10 Resource counts on every path Zero at exit
11 Operation counts: strong versus basic, valid file Both recorded, with the ratio
12 An exception escaping main None; the top-level catch reports it

Boundary cases to run deliberately: an empty file; a file with a header and no records; a valid file whose last line has no newline; a file of exactly the maximum record count.


Expected behaviour

strong import of the truncated file:
  line 4: patient record truncated: "P-004|Zainab"
  patients before 3, after 3
  appointments before 2, after 2
  notes before 1, after 1
  contents identical: yes
basic import of the same file:
  imported 3 patients, then failed at line 4
  patients before 3, after 6      <- the partial import the clinic cannot accept
strong import of the valid file:
  patients 120, appointments 340, notes 88   (one pass)
  operation count: 548 record validations, 0 rollbacks
basic import of the same file:
  operation count: 548 record validations, 0 rollbacks
  ... the difference is only visible on failure: the strong version discarded 3 staged records
commit step: noexcept, no allocation, cannot fail
resources alive at exit: 0

Extension challenge

Make the import resumable: a failed import records the line it failed at, and a later run can continue from a corrected file.

Record in guarantees.md:

  • Whether resumption breaks the strong guarantee, and if so, what the new promise is.
  • Where the resume point is stored, and what happens if the file changes between runs.
  • The new failure modes this introduces (a resume point at a line that no longer exists).
  • The change cost: files, operations, lines — and whether the strong guarantee is still the right promise.

Debugging challenge

The import reports success and the counts have doubled:

int ClinicDatabase::import_from(const std::string& path) {
    std::ifstream in(path);
    std::string line;
    int imported = 0;
    while (std::getline(in, line)) {
        if (line.empty()) { continue; }
        patients_.push_back(parse_patient(line));   // straight into the live database
        ++imported;
    }
    return imported;
}

Reproduce it — import the same file twice, then a bad file — and answer:

  1. Which guarantee is this operation giving, and which one did the requirements ask for?
  2. Why does importing the same file twice hide the problem until the third attempt?
  3. Which line must change to make the staging explicit, and what does the commit then look like?
  4. What would the counts have shown you immediately, if you had printed them before and after?

Reflection questions

  1. Your commit step is noexcept. What exactly cannot fail about it, and what would break that claim?
  2. The strong guarantee made a copy of the staged data. What does that cost for the large file, in the numbers you measured?
  3. Which of the three bad files would have been harder to handle without a staging object? Why?
  4. The clinic's rule is "a failed import changes nothing". Is that always the right promise for an importer? Give a case where a partial import with a report is better, and what the report must contain.
  5. Where does validation happen in your design, and what would it cost to move it earlier — at the parser — or later — at the commit?
  6. Which failure in your design is not recoverable by fixing the file, and how does the operator find out?

Completion checklist

  • [ ] It builds from a documented command with -Wall -Wextra -Werror and no warnings
  • [ ] guarantees.md tabulates the three guarantees with the observed behaviour of each version
  • [ ] All three bad files are refused by the strong version, with line numbers and values
  • [ ] Counts and contents are identical after every failed strong import
  • [ ] The basic version's partial import is demonstrated — the comparison is the evidence
  • [ ] No allocation from an unvalidated count
  • [ ] The commit step is noexcept and does not allocate
  • [ ] Resource counts are zero on every path
  • [ ] Operation counts recorded for the strong and basic versions
  • [ ] All twelve test cases run, with output recorded
  • [ ] The debugging challenge was explained before being fixed
  • [ ] I can state the promise my import makes when it fails, in one sentence

Related: Lab index · Module 13 — Exception Handling · Previous: Practice Lab 25 · Next: Practice Lab 27 — Inventory lookups, measured