ObjectStore C++ Advanced API User Guide

Chapter 1

Advanced Persistence

The information in this chapter augments Chapter 2, Persistence, in the ObjectStore C++ API User Guide. The material is organized in the following manner:

ObjectStore Pvars

When a pointer to persistent memory is assigned to a transiently allocated variable, the value of the variable is valid only until the end of the transaction in which the assignment was made. Using database entry points typically involves looking up a root and retrieving its value - a pointer to the entry point. Frequently this pointer is assigned to a transiently allocated variable for future use. However, its use is limited, since it normally will not remain valid in subsequent transactions (but see Retaining Pointer Validity Across Transactions).

Example of loss of pointer validity outside transaction
      #include <ostore/ostore.hh>
      #include "part.hh"
      void f() {
            objectstore::initialize();
            static os_typespec part_type("part");
            part *a_part_p = 0; 
            employee *an_emp_p = 0;
            os_database *db1 = os_database::open("/thx/parts");
            OS_BEGIN_TXN(tx1,0,os_transaction::update)
                  a_part_p = (part*) (
                              db1->find_root("part_root")->get_value()
                  ); /* retrieval */
                  . . .
            OS_END_TXN(tx1)
            OS_BEGIN_TXN(tx2,0,os_transaction::update)
                  an_emp_p = a_part_p->responsible_engineer; /* INVALID! */
                  . . .
            OS_END_TXN(tx2)
            db1->close();

}
Example of re-retrieving pointers in subsequent transactions
One way to ensure that the pointer remains valid is to re-retrieve the pointer in each subsequent transaction in which it is required.

      #include <ostore/ostore.hh>
      #include "part.hh"
      main() {
            objectstore::initialize();
            static os_typespec part_type("part");
            part *a_part_p = 0; 
            employee *an_emp_p = 0;
            os_database *db1 = os_database::open("/thx/parts");
            OS_BEGIN_TXN(tx1,0,os_transaction::update)
                  a_part_p = (part*) (
                        db1->find_root("part_root")->get_value()
                  ); /* retrieval */
                  . . .
            OS_END_TXN(tx1)
            OS_BEGIN_TXN(tx2,0,os_transaction::update)
                  a_part_p = (part*) (
                        db1->find_root("part_root")->get_value()
                  ); /* re-retrieval */
                  an_emp_p = a_part_p->responsible_engineer; /* valid */
                  . . .
            OS_END_TXN(tx2)
            db1->close();
      }

Using Pvars to Maintain Pointer Validity

A convenient alternative is to use ObjectStore pvars. ObjectStore pvars allow you to maintain, across transactions, a valid pointer to an entry point object.

To use pvars, you define the variable you want to hold the pointer to the entry point. Then you pass the variable's address to the function os_pvar::os_pvar(), along with the name of the root that points to the desired entry point object, and a pointer to the database containing the root.

This function is the constructor for the class os_pvar, but you never have to explicitly use the instance of os_pvar that results. This instance must be a stack object, however, so do not create it with new.

Once you have called the os_pvar constructor, ObjectStore automatically maintains an association between the variable and the entry point. At the beginning of each transaction in the current process, if the database containing the specified root is open, ObjectStore establishes a valid pointer to the entry point object as the value of the variable. It also sets the variable to point to the entry point when the database becomes open during a transaction.

When control leaves the block in which the os_pvar constructor was called, the destructor for the resulting instance of os_pvar is executed, breaking the association between the variable and entry point. Therefore, if you are using the ObjectStore transaction macros, you should call the constructor from outside any transaction, since the macros establish their own block.

Additional Type Safety

As with os_database_root::get_value(), you can also supply an os_typespec* to os_pvar::os_pvar() for additional type safety. ObjectStore will check that the specified typespec matches the typespec stored with the root. Note that it checks only that the typespec supplied matches the stored typespec, and does not check the type of the entry point object itself.

See Type Safety for Database Roots in Chapter 2 of the ObjectStore C++ API User Guide for more information on the use of typespecs.

Pvar Example

Here is an example of the use of pvars:

      #include <ostore/ostore.hh>
      #include "part.hh"
      void f() {
            objectstore::initialize();
            static os_typespec part_type("part");
            part *a_part_p = 0; 
            employee *an_emp_p = 0;
            os_database *db1 = os_database::open("/thx/parts");
            os_pvar p(db1, &a_part_p, "part_root", &part_type);
            OS_BEGIN_TXN(tx1,0,os_transaction::update)
                  . . .
            OS_END_TXN(tx1)
            OS_BEGIN_TXN(tx2,0,os_transaction::update)
                  an_emp_p = a_part_p->responsible_engineer; /* valid */
                  . . .
            OS_END_TXN(tx2)
            db1->close();
      }
Note that, even though you can use this variable from one transaction to the next without re-retrieving its value, you cannot use it in between transactions. As always, you must be within a transaction to access persistent data. In between transactions, ObjectStore automatically sets the variable to 0. The variable is also set to 0 during a transaction if the database containing its associated root is closed.

Note also that you should not try to set the value of this variable, since ObjectStore handles all assignment of values to it. If you want to retrieve different objects at different times through the use of a single pvar, use a pvar to associate a pointer-valued variable with a pointer-valued entry point object. Then you can change what the entry point points to as needed.

Initialization Functions

You can also create an entry point and root using os_pvar::os_pvar(), by supplying a pointer to an initialization function. The function should allocate the entry point object in a given database, and return a pointer to the new object.

This function will be executed upon the call to os_pvar() or at the beginning of subsequent transactions, if the database to contain the root is open and ObjectStore cannot find the specified root in that database. It will also be called when this database becomes open during a transaction, and ObjectStore cannot find the root in that database.

os_pvar constructor declaration
Here is how the os_pvar constructor is declared:

      os_pvar(
            os_database *db, 
            void *location, 
            char *root_name, 
            os_typespec *typespec = 0, 
            void *(*init_fn)(os_database*) = 0
      );
ObjectStore provides three standard initialization functions, os_pvar::init_long(), os_pvar::init_int(), and os_pvar::init_pointer(). These each allocate an object of the appropriate type (long, int, or void*), initialize the object to 0, and return a pointer to it. You can supply either a standard or a user-defined function to the os_pvar constructor.

Example pvar initialization function
Here is an example of the use of a pvar initialization function:

      #include <ostore/ostore.hh>
      #include "part.hh"
      void *part_init(database *db) {
            static os_typespec part_type("part");
            return new(db, part_type) part("part_0");
      }
      void f() {
            objectstore::initialize();
            static os_typespec part_type("part");
            part *a_part_p = 0; 
            employee *an_emp_p = 0;
            database *db1 = database::open("/thx/parts");
            os_pvar p(db1, &a_part_p, "part_root", &part_type, part_init);
            OS_BEGIN_TXN(tx1,0,os_transaction::update)
                  a_part_p->display();
                  . . . 
            OS_END_TXN(tx1)
            OS_BEGIN_TXN(tx2,0,os_transaction::update)
                  an_emp_p = a_part_p->responsible_engineer; /* valid */
                  . . . 
            OS_END_TXN(tx2)
            db1->close();
      }

Creating Object Clusters

Object clusters, like segments, are created explicitly. Just as you create a segment by performing os_database::create_segment() on the database to contain the new segment, you create an object cluster by performing os_segment::create_object_cluster() on the segment to contain the new cluster.

os_object_cluster* create_object_cluster(os_unsigned_int32 size) ;
The function returns a pointer to an os_object_cluster. Unlike segments, however, which are variable sized and expand to accommodate whatever is added to them, clusters have a fixed size. You specify the size in bytes of a new cluster as an argument to create_object_cluster(). This number must be less than 65536, since 64 KB is the maximum cluster size.

      os_segment *seg1;
            . . . 
      os_object_cluster *clust1 = seg1->create_object_cluster(4096) ;
The actual size of the new cluster is the result of rounding the specified size up to the next whole number of pages, minus the platform architecture alignment (see ObjectStore Building C++ Interface Applications).

Do not perform create_object_cluster() on the transient segment.

Allocating a new object within an existing cluster
You can use os_object_cluster::of() to allocate a new object in the same cluster as an existing one:

      os_database *db1; 
      part *an_old_part, 
      . . . 
      part *a_new_part = new( 
                  os_object_cluster::of(an_old_part) ,
                  part_type
      ) part(111);

Setting Data Fetch Policies

An ObjectStore application can control, for each segment, the granularity of data transfers from the Server to the client. When an application dereferences a pointer to an object that is not already resident in the client cache, ObjectStore retrieves from the Server at least the page containing the object. The default behavior is to retrieve just the page containing the object. However, in some circumstances retrieving additional pages can improve performance, if the objects stored nearby in the database are likely to be referenced within a brief period of time.

Differences in granularity between fetch policies
ObjectStore has several fetch policies you can associate with a given segment to control transfer granularity:

Specifying a fetch policy
You specify the fetch policy for segments or databases using the member function set_fetch_policy(), declared as follows:

      enum os_fetch_policy { 
            os_fetch_page, os_fetch_segment, os_fetch_stream };
      void os_database::set_fetch_policy (
            os_fetch_policy, os_int32 bytes); 
      void os_segment::set_fetch_policy (
            os_fetch_policy, os_int32 long bytes);
Using the set_fetch_policy() function on an os_database object changes the fetch policy for all segments in that database, including segments created by the current process in the future.

Note that a fetch policy established with set_fetch_policy() (for either a segment or a database) remains in effect only until the end of the process making the function call. Moreover, set_fetch_policy() only affects transfers made by this process. Other concurrent processes can use a different fetch policy for the same segment or database.

os_fetch_segment Policy

For applications that manipulate substantial portions of small segments, the os_fetch_segment policy is appropriate. Under this policy, ObjectStore attempts to fetch the entire segment containing the desired page, in a single client/server interaction, if the segment will fit in the client cache without evicting any other data. If there is not enough space in the cache to hold the entire segment, the specified number of bytes are fetched, rounded up to the nearest positive number of pages. (Note that if you specify 0 bytes, this will be rounded up, and the unit of transfer will be a single page.) The os_fetch_segment policy is very efficient if a significant portion of the segment will be required, but wastes time and bandwidth if only a few pages will be referenced.

os_fetch_page Policy

If your database contains segments larger than the client cache of your workstation, or if your application does not refer to a significant portion of each segment in the database, you should use the os_fetch_page fetch policy. This policy causes ObjectStore to fetch a specified number of bytes at a time (rounded up to the nearest positive number of pages), beginning with the page required to resolve a given object reference. Appropriate values for the fetch quantum might range from 4 KB to 256 KB or higher, depending on the size and locality of the application data structures.

      os_segment *text_segment; 
      /* The text segment contains long strings of characters */
      /* representing page contents, which tend to be referred */
      /* to consecutively. So tell ObjectStore to fetch them */
      /* 16 KB at a time. */ 
      text_segment->set_fetch_policy (os_fetch_page, 16384);

os_fetch_stream Policy

For special applications that scan sequentially through very large data structures, os_fetch_stream might considerably improve performance. As with os_fetch_page, this fetch policy lets you specify the amount of data to fetch in each client/server interaction for a particular segment. But, in addition, it specifies that a double buffering policy should be used to stream data from the segment.

This means that, when you scan a segment sequentially, after the first two transfers from the segment, each transfer from the segment replaces the data cached by the second-to-last transfer from that segment. This way, the last two chunks of data retrieved from the segment will generally be in the client cache at the same time. And, after the first two transfers, transfers from the segment generally will not result in eviction of data from other segments. This policy also greatly reduces the internal overhead of finding pages to evict.

      os_segment *image_segment; 
      /* The image segment contains scan lines full of pixel data, */
      /* which we're about to traverse in sequence for image */
      /* sharpening. Telling ObjectStore to stream the data from */
      /* the server in 32 KB chunks gives us access to adjacent */
      /* scan lines simultaneously and optimizes client/server traffic. */
      image_segment->set_fetch_policy (os_fetch_stream, 32768);
When you perform allocation that extends a segment whose fetch policy is os_fetch_stream, the double buffering described above begins when allocation reaches an offset in the segment that is aligned with the fetch quantum (that is, when the offset mod the fetch quantum is 0).

When the Fetch Quantum Is Too Large

For all policies, if the fetch quantum exceeds the amount of available cache space (cache size minus wired pages), transfers are performed a page at a time. In general, the fetch quantum should be less than half the size of the client cache.

Using ObjectStore References

ObjectStore references provide an alternative to using pointers. ObjectStore references allow you to override default restrictions on both referring across databases and referring across transactions. References serve as substitutes for pointers, and you can usually use them as if they actually were valid pointers.

ObjectStore references carry some extra cost over the use of pointers. They are larger than pointers (between 8 and 16 bytes, depending on what kind you use), and dereferencing one usually involves a table lookup.

The most generally useful ObjectStore references are instances of the parameterized class os_Reference. More specialized reference classes are discussed in

Example:
os_Reference
Here is an example of using an os_Reference:

      #include <ostore/ostore.hh>
      #include <stdio.h>
      class employee {
            static os_typespec *get_os_typespec();
            . . . 
      };
      class part {
            static os_typespec *get_os_typespec();
            . . . 
            os_Reference<employee> responsible_engineer;
            . . . 
      };
      void f() {
            objectstore::initialize();
            static os_database *db1 = os_database::open("/thx/parts");
            static os_database *db2 = os_database::open("/thx/parts");
            OS_BEGIN_TXN(tx1, 0, os_transaction::update)
                  part *a_part = new(db1, part::get_os_typespec()) part;
                  employee *an_emp = 
                        new(db2, employee::get_os_typespec()) employee;
                  a_part->responsible_engineer = an_emp;
                  printf("%d\n", a_part->responsible_engineer->emp_id);
            OS_END_TXN(tx1)
            db1->close();
      }
Here, the member responsible_engineer is declared to be of type os_Reference<employee>. The class name in angle brackets is the referent type. It indicates that values of responsible_engineer are references to instances of the class employee.

When the employee* (an_emp) is assigned to a_part->responsible_engineer, a reference to this employee* is automatically constructed and stored there. You can use an employee* anywhere a reference<employee> is expected. In general, you can use a T* anywhere a reference<T> is expected, because there is a conversion constructor, os_Reference::os_Reference(T*).

Now you can use the reference to the employee in many contexts requiring an employee*. This is because the class os_Reference overloads the -> operator, and defines a conversion operator so its instances are converted to pointers to instances of its referent type, when appropriate. So you just use the reference as you would a pointer to its referent type, as in the printf statement.

Note that the -> and conversion operators are the only operators with special reference class overloadings, so references do not behave like pointers in the context of other operators, like [] and ++.

Automatic Database Open

If an ObjectStore reference refers to an object in a database that is not open, ObjectStore opens the database automatically when the object is accessed.

Using memcpy() with Persistent os_references and Related Classes

You can use the C++ memcpy() function to copy a persistent os_reference only if the target object is in the same segment as the source object. This is because all persistent os_references use os_segment::of(this) for os_reference resolution processing and the resolution will be incorrect if the os_reference has been copied to a different segment. This restriction holds true for the eponymous types of the parameterized and unparameterized versions of the following classes: os_Reference, os_Reference_protected, and os_Reference_ this_DB.

Resolution by Relative Pathname

As with cross-database pointers, instances of os_Reference store a relative pathname to identify the referent database. See References and Relative Pathnames.

Referring Across Transactions

"Example: os_Reference" shows how references can be used to refer from one database to another. References can also be used to refer across transactions.

In an ObjectStore application, you typically retrieve pointers to persistent objects and store them in transiently allocated variables. You can normally use these pointers only in the transaction in which they were retrieved:

Example of transiently allocated (invalid) pointers
      #include <ostore/ostore.hh>
      #include <ostore/coll.hh>
      #include "part.hh"
      void f() {
            objectstore::initialize();
            os_Set<part*> *part_set;
            part *a_part; 
            os_database *db1 = os_database::open("/thx/parts");
            OS_BEGIN_TXN(tx1, 0, os_transaction::update)
                  part_set = (os_Set<part*>*) (
                        db1->find_root("part_set")->get_value()
                  ); /* retrieval */
                  . . . 
            OS_END_TXN(tx1)
            OS_BEGIN_TXN(tx2, 0, os_transaction::update)
                  a_part = part_set->query_pick(   /* INVALID! */
                        "part", "part_number==123456", db1
                  );
                  . . .
            OS_END_TXN(tx2)
            db1->close();
      }
Here, part_set is a pointer to an entry point, a set of part* pointers. Since part_set is transiently allocated, its value is valid only until the end of the current transaction. So its use in the query in the next transaction is invalid, and will have unpredictable results. If you want to use this pointer in a subsequent transaction, it must be retrieved again:

Example of retrieving a previously allocated pointer
      #include <ostore/ostore.hh>
      #include <ostore/coll.hh>
      #include "part.hh"
      main() {
            objectstore::initialize();
            os_Set<part*> *part_set;
            part *a_part; 
            os_database *db1 = os_database::open("/thx/parts");
            OS_BEGIN_TXN(tx1, 0, os_transaction::update)      
                  part_set = (os_Set<part*>*) (
                        db1->find_root("part_set")->get_value()
                  ); /* retrieval */
                  . . . 
            OS_END_TXN(tx1)
            OS_BEGIN_TXN(tx2, 0, os_transaction::update)      
                  part_set = (os_Set<part*>*) (
                        db1->find_root("part_set")->get_value()
                  ); /* re-retrieval */
                  a_part = part_set->query_pick(   /* OK */
                        "part", "part_number==123456", db1
                  );
                  . . . 
            OS_END_TXN(tx2)
            db1->close();
      }
Using references to avoid re-retrieving previously allocated pointers
But suppose retrieving the required pointers is relatively complicated or expensive, and you need to use them in many transactions. Then it might be preferable to use ObjectStore references:

      #include <ostore/ostore.hh>
      #include <ostore/coll.hh>
      #include "part.hh"
      main() {
            objectstore::initialize();
            part *a_part; 
            os_Reference<os_Set<part*> > part_set_ref;
            os_database *db1 = os_database::open("/thx/parts");
            OS_BEGIN_TXN(tx1, 0, os_transaction::update)      
                  part_set_ref = (os_Set<part*>*) (
                        db1->find_root("part_set")->get_value()
                  ); /* retrieval */
                  . . . 
            OS_END_TXN(tx1)
            OS_BEGIN_TXN(tx2, 0, os_transaction::update)      
                  a_part = part_set_ref->query_pick(
                        "part", "part_number==123456", db1
                  ); /* OK */
                  . . .
            OS_END_TXN(tx2)
            db1->close();
      }
Here, the variable part_set_ref is declared to be of type os_Reference< os_Set<part*> >. The class name in angle brackets, os_Set<part*>, is the referent type. It indicates that part_set_ref refers to an instance of the class os_Set<part*>. When the os_Set<part*>* returned by the get_value() is assigned to part_set_ref, a reference to the os_Set<part*> is automatically constructed and stored in part_set_ref. You can use an os_Set<part*>* anywhere an os_Reference< os_Set<part*> > is expected.

As described above, you can use a T* anywhere a os_Reference<T> is expected, because there is a conversion constructor, os_Reference::os_Reference(T*).

You can also use the os_Reference in any context requiring an os_Set<part*>. As mentioned above, this is because the class os_Reference overloads the * and -> operators, and defines a conversion operator so its instances are converted to pointers to instances of its referent type, when appropriate. So you just use the reference as you would a pointer to its referent type, as when calling query_pick().

Generating One Reference from Another

A reference can be used to generate a text stream, which in turn can be used to generate another reference to the same object. You do this using ::operator <<() and ::operator >>(). For example:

      #include <ostore/ostore.hh>
      #include <iostream.h>
      void dump_part(part *a_part) {
            os_Reference<part> part_ref = a_part;
            cout << part_ref;
      }
      os_Reference<part> read_dump() {
            os_Reference<part> part_ref;
            cin >> part_ref;
            return part_ref;
      }

Using Nonparameterized References

If your compiler does not support class templates, you can use the nonparameterized reference class os_reference. You also should use os_reference if you need a reference to an instance of a built-in type like int or char, since the referent type of an os_Reference must be a class.

os_reference is just like os_Reference, except the conversion constructor used is os_reference(void*) instead of os_Reference(T*). In addition, the conversion operator used is operator void*() instead of operator T*(), which means that you should use a cast to pointer-to-referent type when dereferencing an os_reference.

Example: using nonparameterized references
Here are some examples:

      #include <ostore/ostore.hh>
      #include <stdio.h>
      class employee {
            static os_typespec *get_os_typespec();
            . . . 
      };
      class part {
            static os_typespec *get_os_typespec();
            . . . 
            os_reference responsible_engineer;
            . . . 
      };
      f() {
            objectstore::initialize();
            static os_database *db1 = os_database::open("/thx/parts");
            static os_database *db2 = os_database::open("/thx/parts");
            OS_BEGIN_TXN(tx1, 0, os_transaction::update)
                  part *a_part = new(db1, part::get_os_typespec()) part;
                  employee *an_emp = 
                        new(db2, employee::get_os_typespec()) employee;
                  a_part->responsible_engineer = an_emp;
                  printf("%d\n",(employee*)
                        (a_part->responsible_engineer)->emp_id);
            OS_END_TXN(tx1)
            db1->close();
      }
Example: using nonparameterized references
      #include <ostore/ostore.hh>
      #include <ostore/coll.hh>
      #include "part.hh"
      main() {
            objectstore::initialize();
            part *a_part; 
            os_database *db1 = os_database::open("/thx/parts");
            os_reference part_set_ref;
            OS_BEGIN_TXN(tx1, 0, os_transaction::update)
                  part_set_ref = (os_set*) (
                        db1->find_root("part_set")->get_value()
                  ); /* retrieval */
                  . . . 
            OS_END_TXN(tx1)
            OS_BEGIN_TXN(tx2, 0, os_transaction::update)
                  a_part = (part*) (
                        ((os_set*) (part_set_ref))->query_pick(
                              "part", "part_number==123456", db1
                        )
                  ) ; /* ok */
                  . . . 
            OS_END_TXN(tx2)
            db1->close();
      }

References and Relative Pathnames

As is true with cross-database pointers, instances of os_Reference store a relative pathname to identify the referent database. The pathname is relative to the lowest common directory in the pathnames of the referent database and the database containing the reference. For example, if a reference is stored in /A/B/C/db1 that refers to data in /A/B/D/db2, the lowest common directory is A/B, so the relative pathname ../../D/db2 is used.

This means that if you copy a database containing an os_Reference or os_reference, the reference in the copy and the reference in the original might refer to different databases. To change the database a reference refers to, you can use the ObjectStore utility oschangedbref, which is described in oschangedbref: Changing External Database References in ObjectStore Management.

Note that an intradatabase reference (a reference referring to an object in the same database) uses a pathname of the form ../db-name, where db-name is the terminal component of the database's pathname. So if you copy or move this database in such a way as to change the terminal component of its pathname - say by making a copy within the same directory - the reference in the copied or moved database will not be an intradatabase reference; it will refer to the location ../db-name, which has the same terminal component as the original. To make sure you get an intradatabase reference, use os_Reference_this_DB or os_Reference_local. See ObjectStore Lightweight References.

A discussion on how relative pathnames work can be found in Cross-Database Pointers and Relative Pathnames in Chapter 2 of the ObjectStore C++ API User Guide.

ObjectStore Lightweight References

Instances of the class os_Reference take up 12 bytes of storage each. If you are using a large number of references, you might be able to use one of the lightweight reference types to save space.

There are three kinds of lightweight references:

A summary of the various ObjectStore reference classes is presented in the tables in Summary of ObjectStore Reference Types.

Local References

Each os_Reference stores a database pathname. An os_Reference_local, in contrast, saves space by not storing the pathname of the referent database, and instead requiring you to supply a pathname when dereferencing it. So you dereference an os_Reference_local explicitly, with a call to the function os_Reference_local::resolve(), which takes a database* argument.

Example: use of os_Reference_local
Here is an example:

      #include <ostore/ostore.hh>
      #include <stdio.h>
      class employee {
            static os_typespec *get_os_typespec();
            . . . 
      };
      class part {
            static os_typespec *get_os_typespec();
            . . . 
            os_Reference_local<employee> responsible_engineer;
            . . . 
      };
      f() {
            objectstore::initialize();
            static os_database *db1 = os_database::open("/thx/parts");
            static os_database *db2 = 
                  os_database::open("/thx/employees");
            OS_BEGIN_TXN(tx1, 0, os_transaction::update)
                  part *a_part = new(db1, part::get_os_typespec()) part;
                  employee *an_emp = 
                        new(db2, employee::get_os_typespec()) employee;
                  a_part->responsible_engineer = an_emp;
                  printf("%d\n",
                   a_part->responsible_engineer->resolve(db2)->emp_id);
            OS_END_TXN(tx1)
            db1->close();
      }
Just as os_reference is the nonparameterized version of os_Reference, os_reference_local is the nonparameterized version of os_Reference_local. Resolving an os_reference_local is just like resolving an os_Reference_local; no cast is necessary (as is necessary with an os_reference), since resolution is explicit.

Using Transient References with os_Reference_transient

Instances of os_Reference_transient are used to refer across transactions, but can only refer from transient memory. So if you need a reference that can be transiently allocated, you can use an os_Reference_transient. No explicit resolution is necessary, since such a reference stores a database* (which saves space compared to storing a database pathname). You use it just as you would an os_Reference, except that it cannot be allocated in persistent storage (since the database* it contains actually points to a transient object).

There is also a nonparameterized class, os_reference_transient, for referring from transient memory. You use it just as you would os_reference, except that it cannot be allocated in persistent storage.

Reducing Relocation Overhead

Using ObjectStore references has an advantage not yet discussed: they can reduce the amount of virtual memory that ObjectStore reserves during certain transactions. When ObjectStore retrieves the objects in a particular database segment, it reserves virtual memory addresses for all the objects in segments pointed to by objects in the retrieved segment. In other words, virtual memory is preassigned corresponding to each outgoing pointer in the retrieved segment (where an outgoing pointer points to an object in a different segment, not necessarily a different database). Whenever an outgoing ObjectStore reference is used in a segment instead of an outgoing pointer, this reduces the amount of virtual memory that must be reserved by transactions that retrieve the segment.

If you want to use a reference solely for the purpose of saving on virtual memory addresses, you can use an os_Reference_this_DB. When ObjectStore resolves one of these references, it assumes the referent is in the same database as the reference itself. So, even though these references save space by not storing a database pathname, they do not have to be resolved explicitly. You use an os_Reference_this_DB just as you would an os_Reference.

There is also a nonparameterized class os_reference_this_DB, which you use just as you would os_reference.

ObjectStore Protected References

You can ensure referential integrity for ObjectStore references by using protected versions of the reference classes. Once an object referred to by a protected reference is deleted, use of the protected reference causes an err_reference_not_found exception to be signaled. If the referent database has been deleted, err_database_not_found is signaled. If you do not use protected references, then, as with regular C++ pointers, you can access arbitrary memory by using a reference whose referent has been deleted.

The protected reference classes are

Do not use protected references to refer to transient memory.

Each time you create a protected reference, a write to the database is performed, to maintain a persistent table that associates protected references with their referents.

You can test a safe reference to see if its referent has been deleted with the member function deleted().

A summary of the various ObjectStore reference classes is presented in Summary of ObjectStore Reference Types.

Summary of ObjectStore Reference Types

Parameterized reference classes
The table below summarizes the characteristics of the various parameterized ObjectStore references.
Class PurposeResolutionReference AllocationReferentAllocationSize inBytes
os_Reference Refer across databases and transactionsImplicitAnywhereAnywhere12
os_Reference_local Refer across databases and transactionsresolve(
referent-database*)
AnywhereAnywhere8
os_Reference_transient Refer across transactionsImplicitTransient memoryAnywhere8
os_Reference_this_DB Save on VM addressesImplicitSame database as referentSame database as reference8
os_Reference_protected Refer across databases and transactions; referential integrityImplicit; signals error if referent deletedAnywherePersistent memory12
os_Reference_protected_local Refer across databases and transactions; referential integrityresolve(
referent-database*)
signals error if referent deleted
AnywherePersistent memory8

Nonparameterized reference classes
The table below summarizes the characteristics of the various nonparameterized ObjectStore references.
Class PurposeResolutionReference AllocationReferent AllocationSize inBytes
os_reference Refer across databases and transactionsCast to pointer to referent typeAnywhereAnywhere12
os_reference_local Refer across databases and transactionsresolve(
referent-database*)
AnywhereAnywhere8
os_reference_transient Refer across transactionsCast to pointer to referent typeTransient memoryAnywhere8
os_reference_this_DB Save on VM addressesCast to pointer to referent typeSame database as referentSame database as reference8
os_reference_protected Refer across databases and transactions; referential integrityCast to pointer to referent type; signals error if referent deletedAnywherePersistent memory12
os_reference_protected_local Refer across databases and transactions; referential integrityresolve(
referent-database*)
; signals error if referent deleted
AnywherePersistent memory8

Retaining Pointer Validity Across Transactions

If you are converting an existing application to use ObjectStore, it might be inconvenient to rewrite the code to use references, especially if the application will use many short transactions. ObjectStore has a feature that enables you to retain persistent addresses across transaction boundaries, so that it is not necessary to use references.

The advantage of this feature is that code is easier to port to ObjectStore. The disadvantage is that ObjectStore might run out of available persistent addresses if too much persistent data is referenced. This is because ObjectStore normally unmaps all persistent data from virtual memory at the end of each transaction. This feature disables that unmapping. In addition, database access might be somewhat slower, particularly if multiple databases are referenced.

The static member function objectstore::retain_persistent_addresses() globally enables retaining persistent addresses. It has no arguments.

The static member function objectstore::release_persistent_addresses() globally releases all persistent addresses. After this function is called, all existing transient-to-persistent pointers are invalidated.

You can determine whether persistent addresses are currently retained with objectstore::get_retain_persistent_addresses(), which returns nonzero for true and 0 for false.

Example: retrieving persistent addresses
Here is an example:

      #include <ostore/ostore.hh>
      #include "part.hh"
      main() {
            objectstore::initialize();
            os_database *db1;
                  . . .
            person *p, *q;
                  . . .
            objectstore::retain_persistent_addresses();
            OS_BEGIN_TXN(tx1,0,os_transaction::update)
                  p = (person *) (db1->find_root("fred")->get_value());
                  /* Now p is valid, and can be referenced normally. */
                  p->print_info();
            OS_END_TXN(tx1)
            /* p cannot be dereferenced outside a transaction, but it */
            /* can be stored anywhere. */
            q = p;
            OS_BEGIN_TXN(tx1,0,os_transaction::update)
                  /* If persistent addresses were not retained, we */
                  /* could not do this without using references for p and q */
                  q->print_info();
            OS_END_TXN(tx1)
      }

Discriminant Functions

For each union type intended to have persistent instances, you must supply an associated discriminant function. Discriminant functions allow ObjectStore to determine the actual layout of any persistent object when mapping it into memory.

Consider the following union:

      union myunion {
            struct {
                   int i;
                   foo * fptr;
            } S1 ;
            struct {
                   bar * btr;
                   float f;
            } S2 ;
      } ;
ObjectStore sometimes has the task of modifying all pointers embedded in an object when the object is brought into memory (see ObjectStore Memory Mapping Architecture in Chapter 1, ObjectStore Concepts, of the ObjectStore C++ API User Guide). In the case above, such an object will contain a pointer either at offset 0 or at offset 4 bytes, depending on whether S1 or S2 is the correct interpretation of the object's structure.

Because the application can reconfigure the object (switch between S1 and S2) at will, the application must record the state of the object relative to layout, and provide a functional interface for extracting that information. This functional interface is the discriminant function.

Defining and using unions with discriminant functions
The union above can be modified slightly to accommodate this as follows:

      union myunion {
            int Tag;
            struct {
                  int T;
                  int i;
                  foo * fptr;
            } S1 ;
            struct {
                  int T;
                  bar * btr;
                  float f;
            } S2 ;
            os_int32 discriminant();
      } ;
      os_int32 myunion::discriminant() { return Tag; }
Applications that use this union type must take care to do

      S1.T = 1; 
when they want to use the S1 layout.

Likewise, when they want to use the S2 layout, they should do

      S2.T = 2; 
Switching layouts must be accompanied by reassigning the leading integer.

Example: defining and using a class with a union-valued data member
You can define a class with a union-valued data member as follows:

      class myclass {
            public:
            myunion MyU;
            int Tag;
            os_int32 discriminant();
      };
      os_int32 myclass::discriminant() { return Tag; }
Now applications must take care that the Tag records the value of the union layout. For example:

       myclass MyC;
       MyC.Tag = 1; MyC.S1.i = 0;
       /* . . . later on we switch */
       MyC.Tag = 2; MyC.S2.f = 1.1;
In the first example, the name of the discriminant function, myunion::discriminant, serves to associate the function with the union myunion. In the second example, the name myclass::discriminant serves to associate the function with the one and only union-valued data member of myclass. When a class has more than one union-valued data member, the name of each discriminant function should have the form

      discriminant_union-name_data-member-name
where union_name is the name of the associated union, and data-member-name is the name of the associated data member.

For heterogeneity considerations, in the ObjectStore C++ API Reference, Chapter 5, User-Supplied Functions, see Discriminant Functions for additional details.



[previous] [next]

Copyright © 1997 Object Design, Inc. All rights reserved.

Updated: 03/31/98 15:27:23