The Ode Active Database: Trigger Semantics and Implementation Daniel F. Lieuwen, Narain Gehani, and Robert Arlein AT&T Bell Laboratories 600 Mountain Avenue Murray Hill, NJ 07974 Abstract
Triggers are the basic ingredient of active databases. Ode triggers are event-action pairs. An event can be a composite event (i.e., an event composed from other events). Composite events are detected by translating the event speci cations into nite state machines. In this paper, we describe the integration and implementation of composite event based triggers into the Ode object database. We focus on implementation details such as the basic trigger events supported, the ecient posting of these events, the handling of transactionrelated events, and the integration of triggers into a real database. We also describe the run-time facilities used to support trigger processing and describe some experiences we gained while implementing triggers. We illustrate Ode trigger facilities with a credit card example.
1 Introduction
Ode [1, 2] is a database system and environment based on the object paradigm. The database is de ned, queried, and manipulated using the database programming language O++, which is an upwardcompatible extension of the object-oriented programming language C++ [19]. O++ extends C++ by providing facilities suitable for database applications, such as facilities for creating persistent and versioned objects, de ning and manipulating sets, organizing persistent objects into clusters, iterating over clusters of persistent objects, and associating triggers with objects. Triggers are the basic ingredient of active databases. Ode triggers have the form event-expression ==> action
An event is any \happening of interest". In a database context, an event is either a simple operation involving the database (e.g., reading or modifying an object, preparing to commit a transaction) or some combination of the simple operations using the operators of an event language. The action is executed after the event speci ed by the event expression is detected. An action can be an arbitrary O++ statement. Triggers are speci ed in O++ class de nitions (Section 4 has an extended example of the syntax). The paper is organized as follows. Section 2 gives a brief overview of O++. We assume throughout the paper that the reader is familiar with C++. How-
ever, we give a brief review of the key C++ concepts used in this paper at the beginning the Section 2. Section 3 lists our design goals. Section 4 illustrates the Ode trigger facilities by giving a non-trivial example of triggers for credit card monitoring. Section 5 describes the run-time date structures necessary for implementing the triggers. Section 6 describes some lessons we learned while building the system. Section 7 describes related work. Finally, Section 8 contains conclusions and describes some of our future work.
2 Ode: A Brief Review
The primary interface to the Ode object-oriented database system is the O++ language which is an extension of the C++ programming language. We brie y review those C++ features and O++ extensions that are relevant to the design of active facilities. The C++ object facility is called the class. Class declarations consist of two parts: a speci cation (type) and a body. The class speci cation can have a private part holding information that can only be used by its implementor, and a public part which is the type's user interface. The body consists of the de nitions of functions declared but not de ned in the class speci cation. For example, here is a speci cation of the class Person: class Person f Name nm; public: Person(Name name); Name name() const; g;
C++ supports inheritance, including multiple inheritance. Inheritance is used for object specialization. The specialized object types inherit the properties of the base object type, (i.e. the data and functions of the \base" object type). As an example, the class Customer can derived from class Person. Customer is the same as Person except that it contains additional data and additional member functions. O++ extends C++ by providing facilities to create persistent objects. O++ visualizes memory as consisting of two parts: volatile and persistent. Volatile objects are allocated in volatile memory and are the same as those created in ordinary programs. Persistent objects are allocated in persistent store, and they continue to exist after the program creating them has
terminated. Each persistent object is identi ed by a unique identi er, referred to as a pointer to a persistent object. Persistent objects are allocated and deallocated in a manner similar to heap objects. Persistent storage operators pnew and pdelete are used for the creation and deletion of persistent objects instead of the heap operators new and delete. Persistent objects can be copied to volatile objects and vice versa using simple assignments: Person *pers = new Person("Robert"); persistent Person *ppers = pnew Person("Narain"); *pers = *ppers; /*copy object pointed to by ppers to the object pointed to by pers*/ *ppers = *pers; /*and vice versa */
Components of persistent objects are referenced like the components of volatile objects, e.g., Name nm = ppers->name();
The O++ compiler compiles an O++ program into C++ code which is compiled by a C++ compiler and linked with the standard libraries and the Ode object manager. The object manager provides run-time facilities for transactions, dereferencing persistent pointers, trigger processing, etc. The object manager is built on top of a storage manager which provides much of the required database functionality such as locking, logging, transactions, etc. The Ode object manager is currently supported on both the disk-based storage manager EOS [3] (for regular Ode) and the mainmemory storage manager Dali [15] (for MM-Ode [11]). A complete description of the O++ language, including all trigger facilities (due to space limitations only a subset of the O++ trigger facilities are described in this paper), can be found in the documentation included with the current Ode release. If interested in acquiring a copy of the Ode release, send email to
[email protected].
3 Design Goals
When designing the Ode trigger facilities, we kept the following design goals in perspective: 1. Triggers should be based on both primitive (basic) and composite events. The primitive event triggers in most database systems are inadequate for applications such as program trading whose actions are triggered based on patterns of event occurrences as opposed to single basic events. 2. Detection of composite events should be ecient. 3. The overhead associated with triggers should be paid only by objects of classes with triggers. 4. Since triggers are associated with persistent (database) objects, the trigger facilities should not add any overhead to volatile object accesses. 5. Adding (deleting) triggers to (from) a class or modifying an existing trigger de nition should not change the persistent object storage layout. Otherwise, such changes will require data conversion.
4 Credit Card Monitoring Example
Each credit card issued by a bank is represented as an object of type CredCard. This class also provides triggers to facilitate monitoring credit usage. Class CredCard is de ned as follows: persistent class CredCard f persistent Customer *issuedTo; float credLim, currBal; void RaiseLimit(float amount); int MoreCred() freturn (currBal 0.8*credLim) && GoodCredHist();g public: void PayBill(float amount); int GoodCredHist() const; void Buy(persistent Merchant *store, float amount); void BlackMark(const char *problem, Date date); /*Declaring the events*/ event after Buy, after PayBill, BigBuy; trigger DenyCredit() : perpetual after Buy & (currBal credLim) == fBlackMark("Over Limit", today()); tabort;g trigger AutoRaiseLimit(float amount) : relative((after Buy & MoreCred()),after PayBill) == RaiseLimit(amount); g;
>
>
>
>
In addition to class members (e.g. issuedTo) and member functions (e.g. Buy), class CredCard contains an event declaration and two triggers. All events of interest to the CredCard class must be explicitly speci ed using an event declaration. The declaration event after Buy, after PayBill, BigBuy;
illustrates two kinds of events: member function events (i.e. after Buy and after PayBill) and userde ned events (i.e. BigBuy). Only these events will be posted to persistent CredCard objects. These events will also be posted to objects of classes derived from class CredCard. Member function events are associated with the invocation of member functions. Member function events are automatically posted by the Ode trigger system while user-de ned events must be explicitly posted by the application. The system is responsible for posting the events after Buy and after PayBill immediately after the invocation of Buy and PayBill, respectively. These events are posted only if the speci ed member functions are invoked via persistent pointers.1 We now explain both of the triggers in class CredCard. 1. Trigger trigger DenyCredit() : perpetual after Buy & (currBal credLim) == fBlackMark("Over Limit", today()); tabort;g
>
>
1 Member functions invoked via volatile object pointers or references do not cause events to be posted in order to avoid volatile object use being subject to trigger overhead (See Section 3).
speci es that if a purchase would cause a customer's credit limit to be exceeded, the attempted purchase should be added to the customer's credit history and the purchase prevented. DenyCredit uses the composite event after Buy & (currBal>credLim)
which is composed of the basic event after Buy and the mask (currBal>credLim). It speci es that if a purchase has just been made, then the mask should be evaluated to nd out if the customer's credit limit would be exceeded if this purchase were allowed to complete. If the mask returns true, then the composite event is recognized and the trigger res|it puts a black mark on the customer's credit rating and aborts the transaction (thus preventing the purchase from being made). Because the trigger is marked perpetual, it remains in force after activation until explicitly deactivated. If perpetual were not speci ed, the trigger would be deactivated after ring the rst time. 2. Trigger trigger AutoRaiseLimit(float amount) : relative((after Buy & MoreCred()), after PayBill) == RaiseLimit(amount);
>
is used to automatically raise a customer's credit limit by a predetermined amount if the customer may soon need more credit and has maintained a good credit history. The composite event relative((after Buy & MoreCred()),after PayBill)
speci es that once the composite event after Buy & MoreCred()
has been satis ed, any future occurrences of will satisfy the trigger's composite event. Since this trigger is not perpetual, it will be deactivated after it res.
after PayBill
4.1 Trigger Activation and Deactivation
Triggers must be explicitly activated. Trigger activation looks very similar to a function call. For example, trigger AutoRaiseLimit is activated as follows for an object referenced through the persistent CredCard pointer credcard: credcard->AutoRaiseLimit(1000.0);
Unless an explicit activation (like the one above) is performed, the trigger AutoRaiseLimit will never re for credcard. Trigger activation returns a TriggerId which can be used to deactivate the trigger. Suppose the TriggerId returned by the activation above was stored in AutoRaise. Then,
deactivate(AutoRaise);
deactivates the trigger.
4.2 Coupling Modes
Following the ECA model [8, 17], Ode supplies the immediate, end (deferred), dependent (separate dependent), and !dependent (separate independent) coupling modes. Both our trigger examples used the immediate coupling mode. Such a trigger is red immediately after its composite event has been detected. An end trigger is red in the transaction where its composite event was detected right before the transaction attempts to commit. A dependent trigger's actions are performed in a separate transaction from the one that detected the event. This separate transaction has a commit dependency on the event detecting transaction|it can commit only if the event detecting transaction does. A !dependent trigger's action is also executed in a separate transaction, but the separate transaction has no commit dependency. Thus, the separate transaction can commit even if the event detecting transaction aborts.
5 Implementation Details
We now give an overview of how trigger processing is performed. Section 5.1 explains how trigger events are eciently recognized using extended nite state machines (FSMs). Section 5.2 describes the run-time representation of basic events, while Section 5.3 describes how member function events are posted. Section 5.4 describes how the triggers are compiled into run-time data structures and explains how these data structures are used. Section 5.5 explains the implementation of transaction-related trigger functionality. Finally, Section 5.6 describes the implementation status.
5.1 Recognizing Event Expressions
Event expressions can be compiled into nite state machines (FSMs) that recognize events eciently using the techniques found in [10]. First, we discuss how to recognize events without masks. The basic events included in the event declaration for a class constitute the alphabet for the regular expression language of that class. The repetition operator \*" and the union operator \||" are provided in Ode's event language, although we did not include an example of their use in this paper. The sequence operator is called \," in our event language and \;" in the regular event language. The sequence operator was renamed to make event expressions as syntactically similar to C++ expressions as possible. Regular expressions can be recognized by FSMs using the well known, regular expression to FSM construction [16]. The FSM alphabet is identical for each object of a class. However, each object o of that class sees a dierent stream of alphabet elements|the member function events associated with o and possibly transaction events. (A class definition can express interest in transaction events on behalf of objects of that class. If an object is interested in before tcomplete, then the system will put that event in its event stream just before the system
enters the prepare-to-commit phase. If an object is interested in before tabort, then the system will put that event in its event stream just before the system aborts a transaction in response to a transaction abort request in Ode code.)
5.1.1 Event Expressions without Masks
By default, an event expression is used to search for event subsequences in the event stream that satisfy the event expression. The subsequence can start/end at any point in the stream. Thus, the implementation prepends the event expression (*any) to the userspeci ed event expressions (i.e. (*any, after Buy) is used to detect the occurrence of event (after Buy) in the event stream). Qualifying a trigger's event with ^ prevents the system from pre-pending (*any) and thus speci es search from the activation point with nothing ignored. Complex events are detected using FSMs corresponding to the user-speci ed event expression (with (*any) prepended unless ^ was speci ed).
5.1.2 Event Expressions with Masks
FSMs are extended to handle masks by using mask states which evaluate predicates to produce the pseudo-events True and False and make transitions on these events. For example, the FSM for the event expression in the following trigger trigger AutoRaiseLimit(float amount) : relative((after Buy & MoreCred()), after PayBill) == RaiseLimit(amount);
>
is shown in Figure 1. State 0 is the start state; state 3 is the accept state. State 1 is marked with * to indicate that it must evaluate the MoreCred() mask to produce pseudo-events rather than wait for external events.
5.2 Representing Basic Events
BigBuy || after PayBill
Transaction events and all the events speci ed in declarations are represented internally as instances of type eventRep. For example, the following eventReps are produced for the CredCard class shown in Section 4:
after Buy
0
False after PayBill
after PayBill
event
1* True
BigBuy || after Buy
3
stored with a trigger activation is the trigger type (e.g. AutoRaiseLimit), the state of the FSM associated with the trigger, and the values of the trigger parameters. We have two choices for generating the shared FSM data: 1. The FSMs could be compiled (generated) once when the class containing the triggers is rst processed. The FSMs could then be stored as persistent objects themselves. 2. O++, because of C++ ancestry, requires that each application program contain the complete class de nition of all objects referenced in an O++ program. We can therefore compile the state machines every time we compile an O++ program. We chose to compile an FSM every time because we cannot tell at compile time whether or not a database will contain the FSM (or even determine exactly which databases may be used since an arbitrary string can be passed to the database open routine). Thus, we would have required a central database for trigger information, requiring that each program connect to an additional database, an expense we did not wish to incur. We also selected a strategy for associating the state of the trigger (e.g. the FSM state and the trigger arguments) with the object. Storing the current state of the trigger in the object itself would have violated our design goal of maintaining the same object layout for C++ classes and for O++ classes with triggers and led to a variety of other problems. Consequently, we selected the alternative strategy of storing the state of each trigger separately and using a hash table to map the object to the set of active triggers associated with it. (Note that in either scheme, object accesses that are not updates to the object data members but which advance the FSM will result in an update to either the object or some trigger descriptor|which requires acquisition of a write lock.)
2
BigBuy || after Buy
Figure 1: AutoRaiseLimit's Finite State Machine
5.1.3 Storing Finite State Machines and their State Trigger event expression FSMs are shared by all objects of the class containing the triggers. Most FSM data (e.g. masks, transition functions) are shared. The only FSM-related information that needs to be
static eventRep CredCardEvents[] = f eventRep(0, type CredCard), //BigBuy eventRep(1, type CredCard), //after PayBill eventRep(2, type CredCard) //after Buy g;
type CredCard is the compiler-generated type descriptor for class CredCard. The eventRep constructor assigns a unique integer representation to each underlying event. Because of separate compilation, unique integers cannot be assigned at compile time. Without a database of prior integer assignments, the compiler cannot guarantee that it will assign the same number to the same underlying event in each separately compiled le while ensuring that no two distinct events are mapped to the same numbers. As a
result, the assignment of unique integers to represent events is made at run-time. The eventRep constructor examines a table to see if another eventRep with the same parameters has been constructed. If not, it increments a counter and stores its pair of parameters in the table along with the value of the counter. If so, the current constructor uses the unique integer assigned by the previous constructor. This assignment of unique integers ensures that each underlying event is mapped to exactly one integer and no two distinct events map to the same integer.
5.3 Posting Member Function Events
Ode semantics dictate that only member function invocations on persistent objects, i.e., persistent-pointer ->member-function(...)
cause posting of member function events. The O++ compiler rewrites these invocations of member functions with associated events. For example, given pcred, a persistent CredCard pointer, the statement pcred->PayBill(257.34);
is rewritten as: pcred->PayBillWithPost(257.34);
The member function PayBillWithPost is a \wrapper function" generated by the O++ compiler. The wrapper functions are generated to post before and after member function events (as needed) and to call the associated member functions. In our example, the wrapper function PayBillWithPost calls the associated member function PayBill and posts the event after PayBill (before PayBill is not posted since it was not speci ed as of interest). void CredCard::PayBillWithPost(float amount) f PayBill(amount); PostEvent(CredCardEvents[1],pthis, type CredCard);
g
In posting the event after PayBill, the event identi er for after PayBill is passed along with pthis (a persistent pointer to the object PayBillWithPost was invoked on|analogous to this), and the type descriptor type CredCard (which contains trigger information).2 We use a wrapper function rather than modify the code for PayBill to obey our design principle that non-persistent objects should not have to pay any price for the availability of triggers to persistent objects. Using a wrapper function is also simpler and cleaner. The wrapper function is declared to be virtual if the corresponding member function is virtual. The wrapper function returns the value, if any, returned by the associated member function. The same technique is used in [9, 7, 4]. Passing type CredCard is only an optimization to speed up nding trigger information at run-time. 2
5.4 Run-time Trigger Information
We now describe the structures used to represent triggers at run-time|their states, masks, actions, and FSMs. We will also describe how these data structures are manipulated to activate/deactivate a trigger and to post a basic event.
5.4.1 Trigger States
The trigger state is stored in a persistent data structure, since it must persist across transactions. This data structure uses the following type de nitions: persistent struct TriggerState f unsigned int triggernum; persistent void *trigobj; int statenum; persistent metatype *trigobjtype; g; typedef persistent TriggerState *TriggerId;
A TriggerState's triggernum indicates which trigger (e.g. DenyCredit, AutoRaiseLimit) is being represented. The trigobj is the object the trigger was activated for, and statenum is the current state of the trigger's FSM. The trigobjtype speci es the class the trigger was de ned in; it is needed because of inheritance since an object can have active triggers from several base classes. The machinery for a trigger (e.g. its FSM, its action code, etc.) is stored in the compiler-generated type descriptor (e.g. type CredCard) of the type that de ned the trigger, and trigobjtype is used to nd the relevant type descriptor for processing the trigger. To implement the trigger AutoRaiseLimit, the class CredCardAutoRaiseLimitStruct which inherits from TriggerState is created. It has one new eld amount. This data structure is initialized when a trigger is activated|the trigger arguments are stored, the machine is labeled with the appropriate trigger identi er, and put into the start state. The new trigger is stored in an index that maps an object to all the triggers active on that object, an index used when posting events. An instance of trigger AutoRaiseLimit is activated by calling the following static member function of CredCard; this function initializes the appropriate data structures. static TriggerId AutoRaiseLimit( persistent CredCard *trigobj, im td *triggertype, float amount) f persistent CredCardAutoRaiseLimitStruct *newtrig = pnew CredCardAutoRaiseLimitStruct; /*Add mapping trigobj-- newtrig to trigger index*/ newtrig- amount = amount; newtrig- trigobj=trigobj; newtrig- triggernum = 1; /*AutoRaiseLimit is 2nd trigger--using C array numbering*/ newtrig- statenum = 0; /*start state*/ database *db = database::ofdatabase(trigobj); newtrig- trigobjtype=triggertype- FindMetatype(db); /*Each database has its own metatype object for each type that exists in that database*/ return newtrig;
> >
>
> >
g
The activation of the trigger
>
>
TriggerId AutoRaise = pcred->AutoRaiseLimit(1000.0);
is translated to: TriggerId AutoRaise = CredCard::AutoRaiseLimit(pcred, type CredCard, 1000.0);
The TriggerId can be used to deactivate the trigger. The implementation of deactivate(AutoRaise);
removes the trigger's associated CredCardAutoRaiseLimitStruct from the database. It also removes the mapping from AutoRaise->trigobj to AutoRaise in the index mapping object's to their associated triggers.
5.4.2 Trigger Masks and Actions
A static member function is generated to evaluate each mask. An FSM evaluates one of these functions in a masked state. The FSM posts the pseudo-event True (False) if True (False) is returned. For example, the static member function Pred1AutoRaiseLimit is added to class CredCard for the AutoRaiseLimit trigger's predicate. Trigger actions are similarly encapsulated in member functions. For example, for the trigger AutoRaiseLimit, a static member function named AutoRaiseLimitTriggerFunc is added to the CredCard class.
5.4.3 Trigger FSMs
An FSM is represented as an array of states. Each state has a state number, an indication as to whether or not it is an accept state, the mask to evaluate in that state (or NoMask if no such mask exists), and a pointer to an array of transitions for that state. Each Transition contains an eventnum and a newstate|indicating that when the event represented by eventnum is posted in the state the transition belongs to, move to the newstate. struct Transition f unsigned int eventnum; int newstate; g; class State f enum Accept fNO ACCEPT, ACCEPTg; int statenum; Accept status; MaskFunction mask; Transition *transfunc;
g;
...
Any event which does not appear in a state's Transition list is ignored. Ignoring such events simpli es the posting of events in the presence of inheritance. A base class trigger should not see the events of a derived class. However, it is simplest to have PostEvent treat all events and all active triggers uniformly by always passing an event to each active trigger for an object, whether the trigger is interested in that event or not. Also, since PostEvent does not have to check if a particular event should be posted to
a particular active trigger, the number of data structure probes goes down. That particular check is folded into the FSM move routine, which has to determine which state to move to on the current input anyway.
5.4.4 Trigger Information Container Type
All the information related to a single trigger is packaged up into a TriggerInfo which contains a pointer to a nite state machine, a pointer to a trigger function (e.g. CredCard::AutoRaiseLimitTriggerFunc), an indication as to whether or not the trigger is perpetual, and a coupling mode. An array of two such descriptors, one for each CredCard trigger, is stored in the type descriptor type CredCard (generated by the O++ compiler) which is used by PostEvent to post events and re triggers.
5.4.5 Posting Basic Events
The data structures described thus far are used to post basic events. As an example, consider what occurs during the posting of the event after PayBill| which is implemented in PayBillWithPost by PostEvent(CredCardEvents[1], pthis, type CredCard);
1. An index lookup is performed to nd all the triggers that are active on the object being posted to (i.e. pthis in this example).3 2. For each active trigger (represented as a persistent TriggerState*), trigstate, trigstate->triggernum is used to index into the array of TriggerInfos belonging to type CredCard.4 Suppose the TriggerInfo for AutoRaiseLimit is found. Then, the following actions are performed: a. The trigstate->statenum-th position in the State array that represents AutoRaiseLimit's FSM is examined. That State's array of Transitions, transfunc, is searched until a Transition with eventnum equal to CredCardEvents[1] is found. (If no such Transition is found, the event is ignored.) That Transition's newstate is the index of the next state of the machine. Set trigstate->statenum to that newstate. b. The mask in the newstate-th position of the FSM's State array is examined to nd the mask (if any) that must be evaluated in the new state. The resulting mask function (if any) is passed trigstate. If it returns True (False), the system posts a True (False) pseudo-event which is processed in the same manner that 3 If the object has no active triggers, no lookup is required since the persistent object's control information will indicate that. 4 It rst veri es that trigstate->trigobjtype and type CredCard represent the same class. If not, it does a table lookup to nd the relevant type descriptor and nds the TriggerInfo in that type descriptor instead of in type CredCard.
CredCardEvents[1] was starting in step (a). Potentially, multiple mask events must be posted before the system quiesces. c. Finally, a check is made to see if an accept state has been reached.5 If so, the trigger action function of the TriggerInfo, in this case, CredCard::AutoRaiseLimitTriggerFunc, is called with trigstate, and then, since the trigger is once-only, deactivate(trigstate) is executed. Actually, no triggers are red until all triggers have had the basic event posted. This is to prevent the action of one trigger from aecting the mask of another trigger. Conceptually, immediate triggers that are ready to re are red in parallel as nested transactions. In practice, since Ode currently does not have nested transaction capabilities, the triggers are red sequentially in an unspeci ed order|which maintains the conceptual semantics. Note that a trigger's action can cause another trigger to re, leading (conceptually for now) to nesting transactions more than two levels deep. The processing of triggers with non-immediate coupling modes is described in Section 5.5.
5.5 Transaction-Related Trigger Functionality
Transaction boundaries are important in trigger processing. For example, Ode supplies two transaction events: before tcomplete and before tabort. Ode also supplies coupling modes. This section discusses the implementation of this transaction-related trigger functionality. Posting transaction events before tcomplete and before tabort is a two step process. When an object interested in a transaction event is accessed for the rst time in a transaction, the object is put on a \transaction event object" list (by the function generated by the O++ compiler to handle persistent object access). This list is used to post transaction events during commit and abort processing. A trigger whose action is to be executed as a !dependent transaction is handled as follows. On nding a qualifying event, the system adds the object and an indication of which trigger has been satis ed to a !dependent list. The function handling transaction abort is modi ed to check if the !dependent list is non-empty after nishing all the tasks it normally performs for roll-back. If the list is non-empty, it starts a new system transaction (a transaction not explicitly requested by the user, but required for trigger processing). The system transaction scans the list of !dependent triggers to be executed and executes the relevant actions. Similarly, dependent triggers are put on another list. The routine for committing a transaction scans the dependent list in one transaction and the !dependent list in another, processing them as above. A list is also used for end triggers. Immediately before 5 Note that the event stream may contain several event patterns that both match a trigger's composite event and include the latest basic event. However, the corresponding trigger will re at most once in response to the posting of a single event.
posting before tcomplete events, commit processing scans the end list and executes the relevant actions. Since actions of aborted transactions are rolled back, so are their associated events. Event roll-back is handled using standard transaction roll-back of the triggers' states (e.g. a CredCardAutoRaiseLimitStruct's value is rolled back to the value it had at the beginning of the transaction). Thus, the actions will have no impact unless they caused a !dependent trigger to re, in which case although the actions themselves are rolled back, they may cause a system transaction to make permanent changes to the database.
5.6 Implementation Status
Disk-based Ode has been available to universities that have signed a non-disclosure agreement for quite some time; in fact, several releases have been made. We have completed an initial release of the mainmemory version of Ode, MM-Ode, on top of Dali [15] with full Ode functionality (except for B-trees which do not exist in Dali). It is fully source code compatible with disk-based Ode|they even share the same compiler. The two systems also share a great deal of run-time system code. The object manager for Ode is roughly 7500 lines of code. MM-Ode's run-time system requires only about 1500 lines of code that are unique to MM-Ode. It shares the rest with disk-based Ode. The implementation of Ode's active facilities is complete. The system was released in May 1995. If interested in acquiring a copy of Ode (either the main-memory or the disk version), send email to
[email protected].
6 Experiences
The design and implementation phases provided important feedback to each other. Early on, we decided not to allow the transaction abort statement, tabort, to appear statically outside a transaction block, because then the compiler could not ag tabort statements which could potentially be executed outside the context of a transaction. However, when writing trigger examples, we realized that it would be natural to allow triggers to abort transactions. Since trigger actions are not statically enclosed inside a transaction block, we had to relax the restriction that tabort statements must be statically nested inside transaction blocks. The event after tabort, which was included in our original event language [13], was dropped in the implementation. Aborted transactions should have no direct impact on persistent store (although they may spawn !dependent transactions that do change persistent store). However, if after tabort is an event, then it must be posted, directly causing such changes. Furthermore, even if one wished to allow these odd semantics which treat an abort as a commit which rolls back all changes except those involving the after tabort event, this semantics could only be used with explicit aborts. With aborts caused by process crashes, there is no way to determine which trigger changes are needed.
The event after tcommit [13] cannot be implemented properly without storage manager support for event posting. The after tcommit event for a transaction t1 must be posted in some system transaction that runs after t1 completes. However, then the after(tcommit) event for transaction t1 may be posted after the events of transaction t2 even though t1 serializes before t2 . In other word, the event serialization order may be like: t events t events after tcommit for t 1 2 1
Standard storage managers (e.g. Exodus [5] and EOS [3]) do not give the needed support to avoid such a serialization order. Further, it would be very expensive to ensure that after tcommit will be posted even if the system crashes. The dependent and !dependent coupling modes do not have this problem as they must only begin a transaction that may or may not complete. Reasonable semantics for after commit require the use of a phoenix transaction, one that once started will never stop trying to execute until it has completed|even if it must be restarted after the system crashes. We originally planned to represent each FSM's transition function as a normal two-dimensional array using the current state and an integer representing the posted event to index into an array of (next) states. However, this representation is very space inecient for sparse arrays, so event identi ers had to be reused (each class had its events|some of which may have been inherited|numbered starting at zero). However, while single inheritance of such events is simple, multiple inheritance is not. Since a class may inherit distinct events from two base classes that have been given the same numeric representation, the underlying event represented by an integer depends on the integer itself, the actual class of the object, and the class of the member function whose invocation caused the event to be posted. The events would have to be remapped in some cases to a dierent integer. It was found to be much cleaner to map each event to a unique integer and use a sparse array representation of the transition function. We learned that keeping trigger state within the object itself (the natural thing to do) would have several negative consequences from our perspective|it would have changed object layout and required converting existing data when triggers are added/removed from a class. We also discovered that triggers turn read access into write access, increasing both the amount of time the transactions spend waiting for locks and the likelihood of deadlock.
7 Related Work
There have been a number of active relational systems including POSTGRES [18], Starburst [20], and Ariel [14]. Since they support only a xed number of system events (e.g. delete, insert), they do not have to concern themselves with issues like event representation or the posting of member function events which are important to us. Their rules are applied to sets of
tuples; Ode triggers are rooted at objects. Their rules' actions are always executed in the same transaction as the triggering action; Ode triggers can use all the ECA [8] coupling modes (and more) for determining when to check conditions and perform actions. A variety of techniques for identifying composite events have been proposed: extended FSMs [13], Petri nets [9], and event graphs [6]. While this work used extended FSMs, that technique was not the focus of the work|the focus was on cleanly integrating the syntax of events into C++ and on building an overall runtime system for triggers, of which the event detector is only one part. The goal of REACH [4] is to detect composite events in parallel with the normal application ow. Our current implementation does not support such a composite event detection strategy since we combine event posting with composite event detection. The two could be separated; however, we would then be forced to same restriction that REACH has|namely that composite events can not be used in triggers that must re immediately after the composite event occurs (i.e. they must be red in deferred or detached mode). The overall architecture of Ode and Sentinel [7] is quite similar. Both use a pre-processor to modify the user's code to post events, and both support a similar set of events. Both allow rule activation and deactivation at run-time. Both support composite events, although the languages are somewhat dierent. However, there are also signi cant dierences. Ode's mapping of basic events to globally unique integers is likely to have signi cantly lower event posting overhead than Sentinel's method of representing an event as a triple of strings: the class name, the member function prototype, and the string "begin" (before) or "end" (after). Sentinel currently only supports local composite events [6]|composite events, all of whose constituent basic events take place entirely within a single application program. Ode supports global composite events [6]|composite events whose constituent basic events may span more than one application. (Ode stores TriggerStates in the database, while Sentinel stores its corresponding structures in transient program memory.) Sentinel does not require a rule to be rooted at an object, while Ode does. Given these distinctions, our notions of trigger parameters are quite dierent. In Sentinel, parameters of member function events are collected and stored in transient memory. In Ode, all state associated with a trigger must be accessible to all applications that might interact with that trigger, and thus trigger parameters must be stored persistently. Collecting and persistently storing all basic event parameters, even for a short while, seemed prohibitively expensive. Instead of collecting and storing basic event parameters, parameters are passed in at trigger activation time. Thus, the two projects are complementary, and each could bene t from incorporating technology from the other.
8 Conclusions and Future Work
Triggers are the basic ingredient of active databases. We describe the semantics and implemen-
tation of triggers in the Ode object-oriented database [1, 2]. Ode triggers are based on a model and language for specifying composite events [13, 12, 10]. In this paper, we discussed a wide-range of practical implementation issues related to implementing this model: the set of supported basic events, the ecient posting of basic events, the detection of composite events, the proper handling of transaction-related events, and the integration into an existing database system. We provide these triggers in the context of a system which provides the choice of disk-based or main-memory databases. Including local rules [7] would be useful, since they are low cost and useful for a variety of tasks. No persistent storage is required for such triggers, only data structures that can be deallocated at end-oftransaction. Also, such triggers never require obtaining write locks for the purpose of processing trigger events. They can be used internally to eciently implement constraints. Non-persistent classes may bene t from triggers. We are considering supplying monitored classes, non-persistent classes with triggers|allowing nonpersistent classes to use triggers, while maintaining our design principle that only objects that have access to trigger functionality pay any trigger overhead. We also need to consider attributes of events, allowing each member function event to look at the parameters passed to the corresponding member function, at least in masks. Timed triggers, where the passage of time can be used to produce events, are also of interest. Our current work considers only intra-object triggers, triggers involving a single anchor object. We need to extend this to inter-object triggers where there are several anchoring events so that triggers like \if AT&T goes below 60 and the price of gold stabilizes, buy 1000 shares of AT&T" can be expressed. Finally, we need to support intra- and inter-object constraints as a special case of triggers.
References
[1] R. Agrawal and N. H. Gehani. Rationale for the design of persistence and query processing facilities in the database programming language O++. In Proc. 2nd Int. Workshop on Database Programming Languages, June 1989. [2] R. Arlein, J. Gava, N. Gehani, and D. Lieuwen. Ode 4.0 user manual. Distributed via ftp, 1994. [3] A. Biliris and E. Panagos. Eos: An extensible object store. In Proc. SIGMOD, page 517, May 1994. [4] A. P. Buchmann, J. Zimmermann, J. A. Blakeley, and D. L. Wells. Building an integrated active OODBMS: Requirements, architecture, and design decisions. In Proc. Data Engineering, March 1995. [5] M. J. Carey, D. J. DeWitt, J. E. Richardson, and E. J. Shekita. Storage management for objects in EXODUS. In W. Kim and F. H. Lochovsky, editors, Object-Oriented Concepts and Databases. AddisonWesley, 1989.
[6] S. Chakravarthy, V. Krishnaprasad, E. Anwar, and S.-K. Kim. Composite events for active databases: Semantics, contexts and detection. In Proc. VLDB, August 1994. [7] S. Chakravarthy, V. Krishnaprasad, Z. Tamizuddin, and R. H. Badani. ECA rule integration into an OODBMS: Architecture and implementation. In Proc. Data Engineering, March 1995. [8] U. Dayal, B. Blaustein, A. Buchmann, U. Chakravarthy, M. Hsu, R. Ladin, D. McCarthy, A. Rosenthal, and S. Sarin. The HiPAC project: Combining active databases and timing constraints. SIGMOD Record, 17(1):51{70, Mar. 1988. [9] S. Gatziu and K. Dittrich. Events in an active objectoriented database system. In Proc. of 1st Int. Conf. on Rules in Database Systems, September 1993. [10] N. Gehani, H. V. Jagadish, and O. Shmueli. Compose: a system for composite event speci cation and detection. In N. Adam and B. Bhargava, editors, Lecture Notes in Computer Science, volume 759. SpringerVerlag., 1994. [11] N. Gehani, D. Lieuwen, and S. Sudarshan. MM-Ode: A main-memory object-oriented dbms. In preparation. [12] N. H. Gehani, H. V. Jagadish, and O. Shmueli. Composite event speci cation in active databases: Model & implementation. In Proc. VLDB, August 1992. [13] N. H. Gehani, H. V. Jagadish, and O. Shmueli. Event speci cation in an active object-oriented database. In Proc. SIGMOD, June 1992. [14] E. Hanson. Rule condition testing and action execution in Ariel. In Proc. SIGMOD, pages 49{58, June 1992. [15] H. Jagadish, D. Lieuwen, R. Rastogi, A. Silberschatz, and S. Sudarshan. Dali: A high performance main memory storage manager. In Proc. VLDB, August 1994. [16] H. Lewis and C. Papadimitriou. Elements of the Theory of Computation. Prentice{Hall, Englewood Clis, NJ, 1981. [17] D. R. McCarthy and U. Dayal. The architecture of an active database management system. In Proc. SIGMOD, pages 215{224, May 1989. [18] M. Stonebraker, E. Hanson, and S. Potamianos. The POSTGRES rule manager. IEEE Trans. on Software Eng., 14(7):897{907, July 1988. [19] B. Stroustrup. The C++ Programming Language (2nd Ed.). Addison-Wesley, 1991. [20] J. Widom and S. Finkelstein. Set-oriented production rules in relational database systems. In Proc. SIGMOD, pages 259{270, May 1990.