Tariff catalogue

TariffCatalogue is the manager of all tariff objects as shown in diagram below. This design mainly explains how the tariff catalogue supports multi-threaded applications by prodviding read guards.

Diagram Tariff Objects managed by TariffCatalogue

Tariff objects divide into 5 groups. For each class listed in the right column of table the tariff catalogue contains a list:

1

Tariff system objects
Link service class, tariff class and tariff period to a tariff

TariffSystem, TsVersion
2 Service class objects
Determine a rating criteria based on the used service.
ServiceClass, ScGroup, ScVersion, ScgVersion.
3 Tariff class objects
Determine a rating criteria based on how a service was used.
Origin, Destination, TariffClass, TariffZone, TcSystem, TzConfig, TzgVersion
4 Tariff period objects
Determine a rating criteria based on the timestamp of a network usage.
SpecialDate, DayClass, TariffPeriod, TpGroup, TpgVersion, TpVersion
5 Tariff objects
Compute network usage charges.
Tariff, TfVersion, RatingFunction, RfStaircase, RfSlot
Table Object lists of TariffCatalogue

Use cases

TariffCatalogue has the following responsibilities:

  1. Look-up of tariff objects:
    + Billing: names
    + Rating: functional objects, insure reproducibility of the rating process.
  2. Administration of tariff objects.
  3. Interface to the database:
    The tariff catalog must be not only be able to create and read tariff objects from the database. It also has to ensure consistency when tariff objects are updated or deleted.

Support for multi-threaded applications

The following factors determine the support that the tariff catalogue has to provide for multi-threaded application:

Individual tariff objects are not thread safe, they do not protect access to their data members against simultaneous read and/or write operations. This is best demonstrated by the following typical member function:

const std::string& TariffObject::name() const { return name_; }

Both rating and billing are multi-threaded applications using a TariffCatalogue. The catalog is read during the initialization phase and, once the threads are spawned, used for look up only. (The rating process possibly needs to refresh the tariff catalogue when tariff data are changed, but this discussion can be delayed until we have a comprehensive understanding of the requirements for on-line applications.)

The tariff administration application uses a TariffCatalogue to create, change and delete tariff objects. OSB architecture foresees that online applications use the CORBA interface provided by OSB:

Diagram Deployment of OSB based on-line applications

Because most of the multi-user problems become relevant in on-line applications with an architecture as shown in diagram I'll first discuss these issues and their solution. Later I'll examine how other applications can benefit from the insights that we have gained.

CORBA application

Diagram shows the tariff system server's implementation pattern of CORBA objects:

Diagram Usage of OSB CORBA objects

At a given point in time the CorbaServer creates a CorbaObject cto and publishes its reference1). cto refers to a TariffObject to owned by cat. A servant is responsible to process the requests made by a CorbaClient to cto.

1)

In order to avoid overhead, we only use a single CORBA object, servant and tariff object, to process simultaneous read-only requests of many clients.
These "public" CORBA objects are read only, they do not offer modifying methods: A client that wants to modify an existing tariff object first creates a private, writeable copy of the object, make the changes as needed and updates the catalogue with the modified object to permanently store the changes.

CORBA servants

Naturally, servants for CORBA tariff objects rely on the corrsponding library tariff objects for their implementation. TariffObjectServant sto must remember to which tariff object it refers and, while processing requests, must have access to to that is owned by the tariff catalogue.

Diagram Servant of a tariff CORBA object

A possible implementation of TariffObjectServant::name() is:

CORBA::string TariffObjectServant::name()
{
   CORBA::string rc;
   const TariffObject* p = cat.readLock(oid_);
   if (p) rc = p->name();  // assumes automatic string conversion
   cat.readUnlock(oid_);
   return rc;
}

The read lock on the tariff object ensures, that the tariff catalogue changes neither the pointer to the tariff object or its content while the function executes.

At a first glance it is disturbing that TariffObjectServant must get the pointer to the tariff object in each of its member functions. But consider the alternatives:

  1. Using a pointer to the tariff object owned by the catalogue: const TariffObject* pObject.
    With this approach we still have to aquire a read lock in every function. In addition we'd have to provide some callback mechansim that allows the tariff catalogue to move a tariff object to another memory location without creating dangling references.
  2. Using a copy of the tariff object: const TariffObject object.
    This way, no read locks at all are required. But again we need to provide a mechansim to notify the servant whenever the object in the tariff catalogue changes.

Consistency of servant data

The locking concept as described so far immediately rises one question: How can a CORBA client be sure that the data it has retrieved from a CORBA object are consistent? Or, in other words, how can the client be sure that to was not changed between a call to cto.name() and cto.des()? Should we not allow the CORBA client to read-lock an object for this purpose and at the same time save the locking overhead caused by the two function calls?
The answer to the second question is a simple "No!" for the reasons explained below. Clients that want to be sure having read consistent data will have to use a tariff object's object version:

void CorbaClient::printTariffObject(output o)
{
    int vid = cto.ojectVersion();
    o.print(cto.name());
    o.print(cto.des());
    if (cto.ojectVersion() != vid) {
        // cto has changed.
    }
}

For the avoidance of doubt: the samle code above is supposed to demonstrate the principle. Real-life clients should provide a more stable implementation that properly handles object version changes.

Later we may decide to create a CORBA object not for every tariff object, i.e. oid_ but for each version of that object, i.e.: oid_, objVs_. Clients then have to handle the CORBA exception OBJECT_NOT_EXIST. This approach also requires additional coordination between TariffCatalogue and the controller of the CORBA objects => freezed until ...

Why "No!"?

A CORBA client's connection to its server may get lost. Offering read locks thus would require some kind of lock monitor on the CORBA server side, that automatically releases a lock after a time-out. This has the following consequences.

  1. On the server side we have to make sure that a lock is still active before we access the tariff object.
  2. On the client side we have to verify that the lock remained active during all calls to the CORBA object.

The resulting code could look like this:

CORBA::string TariffObjectServant::desUnlocked()
{
    // If the lock is still active, we use the stored pointer.
    if (cat.isLocked(oid_)) return p->des();
    // Else wrap to the method that locks.
    else return des();
}

void CorbaClient::toNameDes()
{
    int lockId = cto.readLock();
    print(cto.nameUnlocked());
    print(cto.desUnlocked());
    if (lockId != cto.readUnlock(lockId)) {
        // Oops, the lock was lost.
    }
}

As easily can be seen, in terms of coding, error handling and locking overhead there is no benefit at all - and this at the cost of implementing a lock monitor. (And you may have noticed in that there is no warranty in TariffObjectServant::desUnlocked() that the tariff object remains locked during the call to p->des()).

Automatic unlocks: Guards

It is disturbing, that TariffObjectServant::name() must create a local copy of the returned string. Also, the implementation of servants for CORBA tariff object is not always straightforward. It is therefore desirable to have a mechansim that, similar to std::auto_ptr, ensures that the unlock function is called whenever the function exits due to a risen exception or a return statement.

Guard (the term is borrowed from ACE) is a concept that tries to achieve this:

  1. When a guard goes out of scope, it automatically calls the appropriate unlock function.
  2. On copy the ownership of the lock is transferred to the target of the copy.
  3. If a guard is locked, it provides access to the locked object.

With this specification, the following is possible and save:

class Cat {
public:
    typedef ... TariffObjectGuard;
    TariffObjectGuard readGuard(const TariffObject::Oid&);
};

CORBA::string TariffObjectServant::name()
{
    Cat::TariffObjectGuard g = cat.readGuard(oid_);
    const TariffObject* p = g.get();
    // Automatic conversion std::string -> CORBA::string assumed.
    if (p) return p->name();
    else   return "";
}

When written as a template, a guard has the following interface.

// T: type of resource that is locked.
// U: function object that releases the lock.
template<typename T, typename U> class ReadGuard {
public:
    // Constructor with lock flag, locked resource and unlock function object.
    Guard(bool     locked,
          const T* p,
          const U& u);
    // Transfer ownership of lock.
    Guard(Guard& rhs) :
        locked_(rhs.release_()), p_(rhs.p_), u_(rhs.u_) {}
    // Make sure that any held lock is released.
    ~Guard() { unlock(); }
    // Assignment operator not provided.

    // Access to locked resource:
    // returns a NULL pointer if the lock is not held.
    const T* get() const
    {
        if (locked()) return p_; return 0;
    }
    // Is lock still hold?
    bool locked() const { return locked_; }
    // Release the lock.
    void unlock() { if (release_()) u_(); }
private:
    bool     locked_;
    const T* p_;
    U        u_;
    // Return current lock status and set locked_ to false.
    bool release_()
    {
        bool rc = locked_; locked_ = false; return rc;
    }
};

Clients should not make assumptions on the actual implementation of a guard. This is analogous to the iterators of the STL library.

Transparency of locks

By definition TariffCatalogue is the manager of all tariff objects. By this the following two requirements are only natural (at least for an OO novice like me:-):

  1. TariffCatalogue must provide methods to read lock tariff objects. Because only the class itself is allowed to modify objects there is no need to offer public write locks.
  2. Users of the class TariffCatalogue should not be aware of its locking strategy: It should not matter to them (and therefore not require any change of the source code) if TariffCatalogue uses a mutex for each tariff object or only one single mutex for all of them.
  3. For complex objects the following rule must be guarantied by the tariff catalogue: If a complex object is locked, all its references and pointers to other objects are locked too. The rule shall be applicable recursively.
    In the example below, a client that has aquired a lock on a tariff period object can access all of its versions without requesting further locks.
    class TariffPeriod {
    public:
        typedef std::vector<const TpVersion*> Versions;
        const Versions& versions() { return versions_; }
    private:
        Versions& versions_;
    };
    

There are some issues on the above requirement that should be mentionned.

  1. Because clients should be unaware of how the locking is actually done by the tariff catalogue, there is a looming danger of dead-locks. Complete transparency will not always be possible, we might impose some restrictions on the circumstances when clients are allowed to request a lock.
  2. A big part of deadlocks can be avoided when the tariff catalogue uses read-write mutexes that favour readers. This, on the other hand, may lead to starvation of write requests if many readers are using the catalogue.

Tariff object deletion

Until now we have we have not considered how to handle lock requests for deleted tariff objects. Two choices are available: Either we throw an exception or we add a status flag to Guard that allows clients to determine why a lock has failed. The decision what solution will be implemented is delayed until we have a clear picture of the whole implementation of locks.

Another issue under this topic is how to deactivate the corresponding CORBA objects. The problem is that the C++ tariff catalogue doesn't know anything about CORBA, whereas it deletes tariff objects at almost anytime.

Implementation of locks

Separation of concerns

Support for locking will not be implemented in the class TariffCatalogue itself. Instead a new class TcatLockManager will be added to the OSB library. Within an application, only one lock manager should exist.

Diagram Responsibilities of the lock manager

A possible definition of the lock manager's responsibilities is:

typedef ReadGuard<TariffObject*> TariffObjectReadGuard;

class TcatLockManager {
public:
    ...
    TariffObjectReadGuard readLock(TariffObject::Oid id)
    {
        toMutex_.readLock();
        const TariffObject* ptr = cat_.find(id);
        TariffObjectReadGuard g(ptr, toMutex_, true);
        // If the tariff object does not exist:
        // no need to keep the lock.
        if (0 == ptr) g.unlock();
        return g;
    }
    ...
private:
    // The tariff catalogue to protect.
    TariffCatalogue& cat_;
    // Mutex to protect the catalogue's list of tariff objects.
    RwMutex          toMutex_;
};
The template ReadGuard<T*> and RwMutex are defined in "mutex.h". Please refer to the source documentation for details.

Collaboration

As can be seen from the implementation pattern above, TcatLockManager must have access to the tariff catalogue. The other question is, whether or not TariffCatalogue needs the lock manager in the implementation for its create, update and delete methods. Locking is inevitable when the tariff catalogue modifies one of its objects:

int TariffCatalogue::addTariffObject(Session& session, TariffObject& to)
{
     // Create the tariff object in the database.
     TariffObjectGw gw;
     int rc = gw.create(to);
     // Database operations OK, add the tariff object to list.
     if (0 == rc) {
         // tariffObjects_ must be write locked, but who does it?
         tariffObjects_.push_back(to);
     }
     return rc;
}

There are two possible candiates that we can make responsible to write lock the catalogue's list of tariff objects, TariffCatalogue itself or the caller of TariffCatalogue::addTariffObject.
Making the caller responsible has the advantage that TariffCatalogue doesn't have locking overhead, but it also means that the class never modifies its objects in a way that was not expected by the caller#). On the other hand, if the tariff catalogue has get the locks itself for write operations, neither clients nor the lock manager have to know about the catalogue's implementation of the write operation. At the same time we can argue, that a "writeable" tariff catalogue will exist only in a muli-user application; rating, e.g., will never update a tariff object.

Operation Required locking
TariffCatalogue::read() Write lock all lists of tariff objects.
TariffCatalogue::add(TariffObject) Write lock list of tariff objects.
TariffCatalogue::delete(TariffObject::Oid) Write lock list of tariff objects.
TariffCatalogue::update(TariffObject)

Read lock list of tariff objects,
write lock the tariff object.

const TariffObject&
TariffCatalogue::get(TariffObject::Oid) const

Read lock list of tariff objects,
read lock the tariff object.

const T& TariffObject getT() const

Read lock list of tariff objects,
Read lock the tariff object.

Table Operations on tariff objects and required locks

Table lists the operations on tariff catalogue and tariff object classes: We need to define for every operation what needs to be locked. This, in general will be quite intuitive for read locks. For write operation it might be less obvious, as shown by the example of TariffCatalogue::update(...).

Until futher evidence, the locking responsibility is defined as follows:

  • Read operations on the tariff catalogue or on tariff objects: Lock must be aquired by the caller.
  • Write operations on the tariff catalogue: The tariff catalogue must aquire the necessary locks.

In order to avoid deadlocks, a client must never hold a readlock when it calls a write operation of the tariff catalogue.

#) An example for such a behavior would be the following specification: Whenever the tariff catalogue detects a missmatch between its status and the database, it will automatically synchronize with the database.

Dependencies

The logic of the lock manager not only depends on the tariff catalogue's internal data structure, but on all classes owned by the catalogue. Consider:

class ServiceClass {
    ...
};
typedef std::list<const ServiceClass*> ServiceClasses;

class ServiceClassGroup {
    ...
private:
    ServiceClasses members_;
};

class TariffCatalogue {
    ...
private:
    std::list<ServiceClass>      serviceClasses_;
    std::list<ServiceClassGroup> serviceClassGroups_;
};

With the definitions given above, whenever a ServiceClassGroup is locked, the lock manager also must lock all ServiceClass the are member of the locked group. If TariffCatalogue or ServiceClassGroup is changed, the lock manager potentially needs changes too.

Used mutexes

5 read-write mutexes for tariff systems, service classes, tariff classification systems, tariff periods and tariffs:

  1. If several mutexes must be locked, they must be aquired in the listed order.
  2. Prior to locking the row(s) in TARIFF_OBJECT_LOCK, all internal mutex(es) should be write-locked first.

Constant object only

The tariff catalogue mustn't have any public member functions returning a non-const reference or pointer to one of its tariff object.

Readonly flag

Give tariff catalogue a "read-only" flag: If true no objects at all will be locked, guards however will be created with locked set to true.

class TariffCatalogue {
public:
    TariffCatalogue() { readonly = false; }
    void setReadonly()
    {
        if (readonly) return;
        // Get all write locks.
        readonly_ = true;
        // Release all write locks.
    }
private:
    bool readonly_;
}

Once the readonly flag is set, there is no way to unset it again: How could we make sure that there's no reader in the catalogue in the moment we start writing again?
Owners to the tariff catalgue should apply the following pattern:

TariffCatalogue cat;
cat.read(session);
cat.setReadonly();

Refreshing from database

In the database tariff objects may change out of the tariff server's control. Clearly TariffCatalogue must be able to detect such changes and to handle them accordingly: Whenever an update or a deletion of a tariff object is requested, the object's version is checked against the database.

As of 07-Nov-01 the only way to synchronize the tariff catalogue with the status as in the database is to restart the CORBA tariff catalogue server.

Members

The function

bool lock(session, lockId, waitflag);

the functions tries to lock the row that is identified by lockId in the table TARIFF_OBJECT_LOCK. If the lock fails and waitflag is set to true, the function waits until the row becomes available. The function returns true if the requested row could be locked, else false and throws in case of a database error.

Database

Table definition

TARIFF_OBJECT_LOCK   Temporary locks for groups of tariff objects.
LOCK_SEQUENCE integer not null

Primary key and sequence number: It determines the order in which the tariff object groups are to be locked if this is needed for several groups (lowest number first).

DES varchar2 Description of tariff object group and locked tables.

TARIFF_OBJECT_LOCK
LOCK_SEQUENCE DES
10 Tariff Systems
20 Service Classes
30 Tariff Classification Systems
40 Tariff Period Groups
50 Tariffs