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.
- A patient database holding patients, appointments and notes, with its own invariants (unique patient identifiers, appointments belonging to existing patients, notes attached to appointments).
- An importer that reads a documented text format and either applies everything or nothing.
- 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.
- A demonstration of all three guarantees, in three versions of the same operation:
- the basic guarantee (no crash, no leak, but a partial import is visible),
- the strong guarantee (all or nothing),
- the no-throw guarantee for the final commit step, which must be
noexceptand demonstrably unable to fail. - 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.
- 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.
- 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
noexceptis 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
noexcepthonestly. - Reject untrusted input with contextual messages rather than crashes.
- Measure the cost of a strong guarantee against a weaker one.
Functional requirements¶
- The database's invariants hold after every operation, including after a failed import.
- A failed import leaves all three collections unchanged — counts identical, and their contents compared, not only counted.
- Success on the valid file imports every record, in file order, with a summary printed.
- Every refusal names: the line number, the record kind, and the offending value.
- 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.
- No allocation is made from an unvalidated count in the file.
- The commit step is
noexceptand does not allocate. - The basic version's partial import is visible in its counts — that is what the comparison is for.
- The large valid file imports in one pass, and its timing or operation count is recorded for the comparison.
- 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:
- Write the three guarantees with an example of each from your design — including the one you will implement badly on purpose.
- Identify the commit step and write what could not fail about it.
- Write the failure-context structure you will carry in the exception, and the four fields it needs.
- Predict the counts before and after for each of the three bad files, and for the good one.
Implementation tasks¶
- Implement the database and its invariants.
- Implement the textual format's parser with full validation, including a count check.
- Implement the basic version and record its partial import.
- Implement the strong version with the separate staging object and the
noexceptcommit. - Write the four test files (three bad, one good) as data in the program or as strings; run all four through both versions.
- Compare the counts and the operation counts, and record both.
- 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:
- Which guarantee is this operation giving, and which one did the requirements ask for?
- Why does importing the same file twice hide the problem until the third attempt?
- Which line must change to make the staging explicit, and what does the commit then look like?
- What would the counts have shown you immediately, if you had printed them before and after?
Reflection questions¶
- Your commit step is
noexcept. What exactly cannot fail about it, and what would break that claim? - The strong guarantee made a copy of the staged data. What does that cost for the large file, in the numbers you measured?
- Which of the three bad files would have been harder to handle without a staging object? Why?
- 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.
- Where does validation happen in your design, and what would it cost to move it earlier — at the parser — or later — at the commit?
- 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 -Werrorand no warnings - [ ]
guarantees.mdtabulates 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
noexceptand 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