Chronos examples

Two examples are included in the source archive of Chronos, in the example directory. The first example (example.cc) is a compilation of all code examples given in the API description. The second example (interactive-example.cc) is a small interactive application using Chronos to store short texts.

Compile and run

To compile and execute these examples, you first need to download and install Chronos.

Then, in the example directory, you can compile both examples with the command:

$ make

To run the first example:

$ ./example

To run the second example:

$ ./interactive-example

Example 1: introduction to Chronos API

This first example uses Chronos to insert, update, delete and read key-value pairs. To that end, it creates a new table "samples", which is deleted when the application exits.

Headers

Chronos function prototypes are declared in multiple header files. An application has to include the base header file chr_chronos.h to use the library.

#include "chr_chronos.h"

Initialize the database

A Chronos database is associated with a device (here a dummy file in the /tmp directory) and a persistence folder (here also located in the /tmp directory). Chronos creates all necessary files, including the device if needed, but the persistence directory has to be created beforehand.

/* init a database */
chr_chronos * chronos = new chr_chronos("/tmp/chronos_dummy_flash_device",
    "/tmp/chronos_dummy_persistence_folder/");

Note that the database can be reset by deleting the device and persistence files, here:

  • /tmp/chronos_dummy_flash_device
  • /tmp/chronos_dummy_persistence_folder/chronos.chr
  • /tmp/chronos_dummy_persistence_folder/device.chr

Create the table

The "samples" table stores records whose key is made up of a sensor id (a 32 bit unsigned integer) and a timestamp (a 64 bit unsigned integer), and the value is a double. To create such a table, the application must specify the size of keys and values.

/* create a table */
chronos->create_table("samples", sizeof(uint32_t) + sizeof(uint64_t), sizeof(double));

Open the table

To open a previously created table, only its identifier has to be specified.

/* open a table */
chr_table_ctx * samples_ctx = chronos->open_table("samples");

Open a cursor

When a cursor is opened, a position has to be specified. The cursor can be positioned on the first or last key-value pair of the table, or a specific key. In this example the cursor is positioned right before the key key, since the next operation made by the application will occur at this position.

/* open a cursor */
chr_cursor * cur = samples_ctx->open_cursor(key, CHR_KEY);

Note that any position for this cursor would have been allowed, but setting the right one is more efficient. Additionally, the specified key does not have to exist in the table for a position to be correct.

Manage key-value pairs

Once a cursor is opened, it can be used to insert, update, delete or read key-value pairs.

/* insert a key-value pair */
cur->insert(key, value);

Update operations are only allowed to update the value associated with a key. The key therefore does not change.

/* update a value */
cur->update(key, value);
/* delete a key-value pair */
cur->del(key);

Scan the table

To insert, update or delete a key-value pair, the cursor can be positioned anywhere: Chronos will move it if needed. However, to read a range or a single pair, the position has to be set just before the first required key. Therefore, for a full table scan, the cursor has to be moved before the first pair of the table. Each pair is then accessed one by one with increasing keys using the function read_next, until CHR_EOF is reached.

/* move a cursor and read key-value pairs */
cur->move(NULL, CHR_FIRST, CHR_BEFORE);
while (cur->read_next(key, value) != CHR_EOF)
{
  [key-value pair processing]
}

Drop the table

A table must be closed before being dropped. Additionally, all of its cursors must be closed beforehand.

/* close a cursor */
samples_ctx->close_cursor(cur);

/* close a table */
chronos->close_table(samples_ctx);

/* drop a table */
chronos->drop_table("samples");

Close the database

The application must close every cursors, tables and databases before exiting. In this example, all cursors and tables are already closed at this point.

/* close a database */
delete chronos;

Example 2: Short texts database

In this example, the user can compose and store small texts (up to 80 characters). Each text is associated with a text id (an unsigned integer) specified by the user and the timestamp at which the text has been written. If a text id already exists, its associated text and timestamp are updated. Texts are accessed using their text id only, which therefore acts as the key. The value consists of two elements: the timestamp and the text itself.

This example loops on the following operations:

  • display the table contents using the native pairs order (increasing keys),
  • let the user enter a new text, a sequence of new texts or exit.

This example illustrates:

  • the primary access methods (insert, update and read),
  • the order key-value pairs are written on the device,
  • the use of structs to store data.

Note that Chronos is usually not appropriate to manage variable-sized data types such as text.

Data structures

Chronos manages sequences of bytes. The application has to convert Chronos data types (unsigned char *) to custom types. In this example, this is done by using structs for keys and values:

typedef struct _key_st {
  uint32_t id;
} key_st;

typedef struct _value_st {
  uint64_t timestamp;
  char text[TEXT_LEN];
} value_st;

When using structs, make sure none of the field types are pointers. Additionally, data alignment within structs can lead to larger keys and values.

Byte order

Chronos uses memcmp to order keys. This function compares bytes of memory blocks with increasing memory addresses, and is not suitable for various data types including little-endian encoded integers. For this function to match the native integer comparators (<, ==, >), integers must be converted to big-endian. In this example, a function is defined to convert a key to and from big-endian format.

/* convert a key_st sruct from big endian to host format */
void betoh(key_st * key) {
  key->id = be32toh(key->id);
}

/* convert a key_st sruct from host format to big endian */
void htobe(key_st * key) {
  key->id = htobe32(key->id);
}

Initialize the database

A Chronos database is associated with a device and a persistence folder, which must be set during the initialization.

chr_chronos * chronos = new chr_chronos("/tmp/chronos_dummy_flash_device",
    "/tmp/chronos_dummy_persistence_folder/");

Initialize the table

A Chronos table is identified by a unique string, "texts" in this example. We first try to open this table. If it does not exist, we create and open it.

/* open the table */
chr_table_ctx * texts_ctx = chronos->open_table("texts");
/* create the table if it does not exist (open failed) */
if (texts_ctx == NULL) {
  [...]
  chronos->create_table("texts", sizeof(key_st), sizeof(value_st));
  texts_ctx = chronos->open_table("texts");
}

To create a new table, the size of keys and values has to be specified. In this example, we use the size of both structs previously defined.

Scan the table

Chronos uses cursors to read key-value pairs from the table. For a full table scan, this cursor is opened on the first key-value pair. The table contents are then accessed, pair by pair, with increasing keys using the function read_next.

/* open a cursor on the first pair */
chr_cursor * read_cur = ctx->open_cursor(NULL, CHR_FIRST);
/* while there is a pair to be read */
while (read_cur->read_next((unsigned char *)&key, (unsigned char *)&value) != CHR_EOF) {
  [key-value pair processing]
}
[...]
/* close the cursor */
ctx->close_cursor(read_cur);

Insert a key-value pair

Just like reading, a cursor has to be opened to insert or update data. We could use the same cursor as the one used for reading, or open a new one. In this example, a single new cursor is used to insert and update all pairs. It is positioned initially on the first pair of the table:

chr_cursor * insert_cur = texts_ctx->open_cursor(NULL, CHR_FIRST);

Note that in Chronos, cursors are optimized to insert consecutive keys (i.e. increasing keys with no prior data between them). When multiple such insertion patterns occur, a cursor should be opened for each concurrent sequence. In this example, this is also the reason why we use distinct cursors for reading and writing (we suppose increasing ids for texts, which is the case at least for batch insertion).

Once opened, a cursor can be used to insert a key-value pair. This cursor does not have to be at the right position for this insertion to succeed, since Chronos does it transparently if needed.

/* insert the pair */
int ret = cur->insert((unsigned char *)&key, (unsigned char *)&value);

After an insertion, the cursor is positioned right after the inserted pair.

Update a value

A cursor can also be used to update the value associated with a key. In this example, since Chronos does not allow duplicate keys, we update the value if the insertion fails:

/* if exists, update it */
if (ret == CHR_DUPKEY) {
  [...]
  cur->update((unsigned char *)&key, (unsigned char *)&value);
}

Close the database

Since Chronos is a prototype, it is currently not quite robust against unexpected operations. For instance, the application must close every cursors, tables and databases for data to be flushed to disk.

/* close cursors, tables and databases */
texts_ctx->close_cursor(insert_cur);
chronos->close_table(texts_ctx);
delete chronos;

Note that during a failure, most of the data is still available and readable from the flash device (data loss is discussed in the original description of Chronos). However, this rebuild operation is currently not implemented.