When building transactionally protected applications, there are some special issues that must be considered. The most important one is that if any thread of control exits for any reason while holding Berkeley DB resources, recovery must be performed to do the following:
Complicating this problem is the fact that the Berkeley DB library itself cannot determine whether recovery is required; the application itself must make that decision. A further complication is that recovery must be single-threaded; that is, one thread of control or process must perform recovery before any other thread of control or processes attempts to create or join the Berkeley DB environment.
There are two approaches to handling this problem:
There are two common ways to build transactionally protected Berkeley DB applications. The most common way is as a single, usually multithreaded, process. This architecture is simplest because it requires no monitoring of other threads of control. When the application starts, it opens and potentially creates the environment, runs recovery (whether it was needed or not), and then opens its databases. From then on, the application can create new threads of control as it chooses. All threads of control share the open Berkeley DB DB_ENV and DB handles. In this model, databases are rarely opened or closed when more than a single thread of control is running; that is, they are opened when only a single thread is running, and closed after all threads but one have exited. The last thread of control to exit closes the databases and the environment.
An alternative way to build Berkeley DB applications is as a set of cooperating processes, which may or may not be multithreaded. This architecture is more complicated.
First, this architecture requires that the order in which threads of control are created and subsequently access the Berkeley DB environment be controlled because recovery must be single-threaded. The first thread of control to access the environment must run recovery, and no other thread should attempt to access the environment until recovery is complete. (Note that this ordering requirement does not apply to environment creation without recovery. If multiple threads attempt to create a Berkeley DB environment, only one will perform the creation and the others will join the already existing environment.)
Second, this architecture requires that threads of control be monitored. If any thread of control that owns Berkeley DB resources exits without first cleanly discarding those resources, recovery is usually necessary. Before running recovery, all threads using the Berkeley DB environment must relinquish all of their Berkeley DB resources (it does not matter if they do so gracefully or because they are forced to exit). Then, recovery can be run and the threads of control continued or restarted.
We have found that the safest way to structure groups of cooperating processes is to first create a single process (often a shell script) that opens/creates the Berkeley DB environment and runs recovery, and that then creates the processes or threads that will actually perform work. The initial thread has no further responsibilities other than to monitor the threads of control it has created, to ensure that none of them unexpectedly exits. If one exits, the initial process then forces all of the threads of control using the Berkeley DB environment to exit, runs recovery, and restarts the working threads of control.
If it is not practical to have a single parent for the processes sharing a Berkeley DB environment, each process sharing the environment should log their connection to and exit from the environment in a way that allows a monitoring process to detect if a thread of control might have acquired Berkeley DB resources and never released them. In this model, an initial "watcher" process opens/creates the Berkeley DB environment and runs recovery, and then creates a sentinel file. Any other process wanting to use the Berkeley DB environment checks for the sentinel file; if the sentinel file exists, the other process registers its process ID with the watcher and joins the database environment. When the other process finishes with the environment, it unregisters its process ID with the water. The watcher periodically checks to ensure that no process has failed while using the environment. If a process does fail while using the environment, the watcher removes the sentinel file, kills all processes currently using the environment, runs recovery, and re-creates the sentinel file.
Obviously, it is important that the monitoring process in either case be as simple and well-tested as possible because there is no recourse if it fails.