Berkeley DB XML
Berkeley DB XML is a powerful, fully transactional, XML-based database that uses XQuery (a W3C standard) as its query language. (Berkeley DB XML does NOT use SQL.)
This page is a tutorial on writing a multi-threaded Berkeley DB XML application in Haskell. It is intended for Haskell programmers who are new to Berkeley DB XML.
I would like to see Berkeley DB XML used with Haskell, because I believe that SQL is old-fashioned and limiting. An advanced programming language should have the power of an advanced database.
Obtaining and building the packages
Berkeley DB XML is easy to build. On Unix, the ./buildall.sh script will build everything for you, including Berkeley DB, and put the resulting image into the 'install' directory. You can then copy this directory's contents to an install location of your choice.
On a GNU/Linux system, you may want to add the 'lib' directory of this install location under /etc/ld.so.conf.d/ then run "ldconfig". This will allow the system to find the Berkeley DB XML libraries. If you don't do this, you will have to set the environment variable LD_LIBRARY_PATH.
If you are using a Unix system, your system may already have a sufficiently recent version of Berkeley DB. In this case, it is better to use this and build Berkeley DB XML only. The commands for this are as follows:
./buildall.sh --build-one=xerces ./buildall.sh --build-one=xqilla ./buildall.sh --build-one=dbxml --with-berkeleydb-prefix=/usr
To test your installation, see if you can run the 'dbxml' command from the install image's bin directory. This is an interactive utility that allows you to run database queries and view the results.
The Berkeley DB XML binding for Haskell is a standard Cabal package. Its README file gives installation instructions.
If you want to use Berkeley DB only, it is easy to disable the DB XML part of the build in the Berkeley DB XML binding package.
This tutorial uses a Haskell binding for DB XML that sticks closely to Berkeley DB XML's C++ interface, so we are programming at a fairly low level.
DB XML would lend itself to the development of higher-level wrappers. For example, someone could write a drop-in replacement for STM (Software Transactional Memory) that uses DBXML to give persistent storage.
Adventure game example
In the Berkeley DB XML binding distribution, you will find a tiny adventure game under examples/adventure/. You need to install the HXT (Haskell XML Toolbox) package for this. This tutorial will refer to the code in this example.
Note that this game is multi-user and the game world, including player locations, is stored persistently, so it survives a re-start of the adventure server.
Here is an example session:
blackh@amentet:~/temp/BerkeleyDBXML-0.2/examples/adventure$ ./adventure Adventure server - please telnet into port 1888 tidying up 0 cadavers Creating the game world...
blackh@amentet:~$ telnet localhost 1888 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. Welcome to 'DB/XML Haskell binding' adventure by Stephen Blackheath Please enter your name. > Stephen Welcome for the first time, Stephen. For help, please type "help". You are on a wide, white sandy beach. A bright blue ocean stretches to the horizon. Along the beach to the north you can see some large rocks. There is thick jungle to the west. You can see a starfish > get starfish You pick up a starfish. > west You are in a dense, jungle. You can see a tall, twisty tree > drop starfish You drop a starfish. > look You are in a dense, jungle. You can see a starfish a tall, twisty tree >
Berkeley DB concepts
DB XML is built on top of Berkeley DB. This section describes the concepts specific to DB.
Berkeley DB is not client-server like most SQL databases. It accesses its database files in a local directory.
An "environment" is a directory with various odd-looking files such as __db.001 and log.0000000001, as well as various database files that your application has created. An application would normally have only one environment, where it would store all its databases. Database transactions operate within an environment, so a single transaction can span multiple database files in the same environment. This also means that you can update both Berkeley DB files and Berkeley DB XML files in a single transaction.
When DB_THREAD is enabled, the environment will work safely with multi-threaded applications, and also multiple processes accessing the databases at the same time. This means you can run the 'dbxml' command-line utility while your application is running.
When the DB_INIT_LOG flag is enabled, the environment contains a transaction log that forms part of the databases. Do not delete these files, or you will corrupt your databases. Also when DB_INIT_LOG is enabled, you cannot move your database files from one environment to another. The recommended way to do this is to use Berkeley's dbxml_dump/dbxml_load for DB XML files and db_dump/db_load for DB files.
A production application must periodically call dbEnv_txn_checkpoint to clear old data from the log.* files. (The adventure game example does not do this.)
It is safe, however, to delete databases you have created without deleting the environment. The environment will detect this and adjust accordingly. You can, of course, start with a clean slate by deleting all the environment files and databases.
Here is some example code which will open an existing environment, or create it if it doesn't exist. These are the flags to use for a transactional, multi-threaded application:
dbenv <- dbEnv_create  -- Enable automatic deadlock detection. dbEnv_set_lk_detect dbenv DB_LOCK_DEFAULT dbEnv_open dbenv "." [DB_CREATE,DB_INIT_LOCK,DB_INIT_LOG,DB_INIT_MPOOL, DB_INIT_TXN,DB_THREAD,DB_RECOVER] 0
Berkeley DB will automatically detect deadlocks for you, allowing you to re-start the deadlocked transaction. Because of the way Berkeley DB has been engineered, deadlock detection is not optional in multi-threaded applications. It is absolutely impossible to avoid deadlocks by carefully controlling the order of locking.
Your application needs one and only one lock detector thread or process running per environment. dbEnv_set_lk_detect is an easy way to spawn one such thread. See the Berkeley DB documentation for other ways.
If your application has more than one process, you can't do it the way this examples does it. You would need to manage things so only one lock detector was running.
Because of deadlock detection, your code must detect deadlocks and re-start the transaction if they are found. Here is some code to do this:
-- Execute the specified code within a database transaction, automatically -- re-trying if a deadlock is detected. inTransaction :: XmlManager -> (XmlTransaction -> IO a) -> IO a inTransaction mgr code = inTransaction_ mgr code 0 where inTransaction_ mgr code retryCount = do trans <- xmlManager_createTransaction mgr  catch (do result <- code trans xmlTransaction_commit trans return result ) (\err -> do xmlTransaction_abort trans let dbErr = getDbError err if (dbErr == Just DB_LOCK_DEADLOCK) && (retryCount < 20) then do hPutStrLn stderr "<<retry deadlocked thread>>" inTransaction_ mgr code (retryCount+1) else ioError err )
This shows the use of a function getDbError which is specific to this Haskell binding. When Berkeley DB returns an error code, the binding will throw a Haskell ioError. The getDbError function extracts the Berkeley DB error code from the ioError. A similar function exists for DB XML-level errors.
Remember that the code above pre-supposes that you have started a deadlock detector. If this hasn't happened, the application will stall and never throw DB_LOCK_DEADLOCK.
Before you start your application, you must run a database recovery to return the database to a consistent state, in case of a dirty shutdown. This can either be done with the db_recover command line utility, or by specifying the DB_RECOVER flag to dbEnv_open.
An environment recovery must run without any other processes accessing the database environment. Therefore it must be performed before you start your application.
Because we are using the DB_RECOVER flag to do our recovery, we could not run multiple processes of 'adventure' at the same time unmodified. If we wanted this application to work with multiple processes, both the DB_RECOVER flag and the dbEnv_set_lk_detect call would need to be removed and run separately before the application was started.
DB XML concepts
Most of what you need to know is covered in the "Getting Started with Berkeley DB XML" guide that comes with the Berkeley DB XML distribution, but here are some notes:
- Berkeley DBXML returns its document contents as text. You need to use an XML library of some kind to handle these. The Haskell binding leaves you free to choose what XML library you want.
To see examples of querying, creating and updating DB XML documents, please read the source code of the adventure example included with the DB XML binding distribution.
UTF-8 encoding is used throughout to encode Unicode text. All String arguments and return values in the binding are in Unicode, except for XML text, which is returned as 8-bit characters in a String data type. Your XML library will convert this to Unicode for you.
The function xmlValue_asString is a case where the caller has to make the right choice. xmlValue_asString converts an XmlValue to a Unicode Haskell String. This is appropriate if you are fetching the text contents of an XML tag, for instance.
However, if you are fetching XML text, you will want to call xmlValue_asString8Bit. This leaves out the conversion from UTF-8 to Unicode, so you can let your XML library convert this to Unicode.
I hope this gets you started painlessly writing DB XML applications. If you have any questions (so I can improve this page), or wish to report bugs in the Haskell binding, please contact me at Stephen Blackheath's anti-spam page.
--Blackh 23:23, 25 September 2008 (UTC)