Search code examples
c++robustness

Are there any good techniques to avoid broken program state in case of unexpected crash?


I have a class UserInterface containing a list of Items (whatever they represent). The content of these Items is too expensive to keep in memory due to possibly big size, so each Item only stores some metadata (description, preview...) for the related data, the data itself is stored on the disk in binary files. There is another class - FileHandler, that is responsible for managing these files. Each Item has id_ associated with the related File. UserInterface has a member reference to FileHandler to be able to get the list of Files from it and represent them as Items on the screen, but FileHandler does not even know about the existence of UserInterface. When the user selects an Item in the UI, UserInerface asks FileHandler to find a File with the same id_ (as the selected Item has), and then this File can be read from the disk for further usage of its content.

But here are some drawbacks of this design:
Let's say the user wants to delete some Items. They should be deleted from both UI and disk storage:

UserInterface::deleteItems(/*list of id's*/)
{
    fileHandler_.deleteFiles(/*list of id's*/);

    // What if a power failure or unexpected crash happens here?

    // Delete items from UI...
}

If something bad happens during the execution of the following method, the next time the user runs the program they will get a broken state - the UI will contain corrupted items linked to files that have been deleted. I can still check if all files listed in the UI exist, and if some of them don't, I can mark corrupted items as invalid/broken, but I would rather prefer to avoid such situations completely.

Are there any good design patterns/techniques aiming to solve such problems?


Solution

  • Your program can stop at any time when the power is lost or the operating system crashes, and there is nothing you can do to avoid that.

    However, it can also stop due to a controlled operating system shutdown or when your own code crashes. You can design your software so that it can close gracefully in some of these cases, by handling the shutdown message from the operating system, catching unhandled exceptions, handling std::terminate, handling close signals, etc.

    If you use a database such as SQLite, and use transactions to write data, it can handle many of these situations for you. However, even SQLite database can get corrupted in some cases. For more information.

    Because you cannot control the situation when the operating system crashes, you also need to fix any problems during your startup sequence. You should design the program so that if cannot fix corrupted database files (or other problems), it can at least detect them. In SQLite, you can execute PRAGMA integrity_check to see whether the database is corrupted. If your program must be able to recover automatically, you could take backups and recover the most recent backup, or you could restore the default settings.