Tariff catalogueTariffCatalogue 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. |
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 |
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 |
TariffCatalogue has the following responsibilities:
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: |
|
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:
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.
Diagram shows the tariff system server's implementation pattern of 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. |
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.
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:
const
TariffObject* pObject
.const TariffObject object
.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 ... |
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.
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()
).
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:
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.
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:-):
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.
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.
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.
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, |
const TariffObject& TariffCatalogue::get(TariffObject::Oid) const |
Read lock list of tariff objects, |
const T& TariffObject getT() const |
Read lock list of tariff objects, |
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:
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.
5 read-write mutexes for tariff systems, service classes, tariff classification systems, tariff periods and tariffs:
The tariff catalogue mustn't have any public member functions returning a non-const reference or pointer to one of its tariff object.
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();
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.
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.
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 |