The GemStone object server’s fundamental mechanism for maintaining the integrity of shared objects in a multiuser environment is the transaction. This chapter describes transactions and how to use them. For further information, see the chapter in the GemStone Programming Guide entitled “Transactions and Concurrency Control.”
Transaction Management: an Overview
introduces the concepts to be explained later in the chapter.
Operating Inside a Transaction
explains the transaction model, committing, and aborting.
Operating Outside a Transaction
discusses a lower-overhead alternative for read-only views of the shared repository.
Transaction Modes
explains the difference between automatic and manual transaction modes.
Managing Concurrent Transactions
discusses concurrency conflicts and ways to minimize them, such as locks.
Reduced-Conflict Classes
describes specialized GemStone collections that minimize conflicts without locking.
Changed Object Notification
explains a mechanism for coordinating the activities of multiple sessions.
The GemStone object server provides an environment in which many users can share the same persistent objects. The object server maintains a central repository of shared objects. When a GemBuilder application needs to view or modify shared objects, it logs in to the GemStone object server, starting a session as described in Chapter 2.
A GemBuilder session creates a private view of the GemStone repository containing views of shared objects for the application’s use. The application can perform computations, retrieve objects, and modify objects, as though it were a single-user Smalltalk image working with private objects. When appropriate, the application propagates its changes to the shared repository so those changes become visible to other users.
In order to maintain consistency in the repository, GemBuilder encapsulates a session’s operations (computations, fetches, and modifications) in units called transactions. Any work done while operating in a transaction can be submitted to the object server for incorporation into the shared object repository. This is called committing the transaction.
During the course of a logged-in session an application can submit many transactions to the GemStone object server. In a multiuser environment, concurrency conflicts can arise and cause some commit attempts to fail. Aborting the transaction discards any changes to persistent objects and refreshes the session’s view of the repository in preparation for further work.
In order to reduce its operating overhead, a session can run outside a transaction, but to do so the session must temporarily relinquish its ability to commit. A session running outside a transaction operates in manual transaction mode, in contrast to the system default automatic transaction mode.
Another potential mode is transactionless mode. However, this mode is not recommended for interactive use, and is not supported by GemBuilder.
GemBuilder provides ways of avoiding the concurrency conflicts that can cause a commit to fail. Optimistic concurrency control risks higher rates of commit failure in exchange for reduced transaction overhead, while pessimistic concurrency control uses locks of various kinds to improve a transaction’s chances of successfully committing. GemStone also offers reduced-conflict classes that are similar to familiar Smalltalk collections, but are especially designed for the demands of multiuser applications.
This chapter explains each of the topics mentioned here: transactions, committing and aborting, running outside a transaction, automatic and manual transaction modes, optimistic and pessimistic concurrency control, and reduced conflict classes. Be sure to refer to the related topics in the GemStone Programming Guide for a full understanding of these transaction management concepts.
While a session is logged in to the GemStone object server, that session maintains a private view of the shared object repository. To prevent conflicts that can arise from operations occurring simultaneously in different sessions in the multiuser environment, Each session’s operations are encapsulated in a transaction. Only when the session commits its transaction does GemStone try to merge the modified objects in that session’s view with the main, shared repository.
Figure 5.1 shows a client image and its repository, along with a common sequence of operations: (1) faulting in an object from the shared repository to Smalltalk, (2) flushing an object to the private GemStone view, and (3) committing the object’s changes to the shared repository.
The private GemStone view starts each transaction as a snapshot of the current state of the repository. As the application creates and modifies shared objects, GemBuilder updates the private GemStone view to reflect the application’s changes. When your application commits a transaction, the repository is updated with the changes held in your application’s private GemStone view.
For efficiency, GemBuilder does not replicate the entire contents of the repository. It contains only those objects that have been replicated from the repository or created by your application for sharing with the object server. Replicated objects are updated only when modified. This minimizes the amount of data that moves across the boundary from the Gem to the client Smalltalk application.
When an application submits a transaction to the object server for inclusion in the shared repository, it is said to commit the transaction. To commit a transaction from the client, send the message:
aGbsSession commitTransaction (to commit a specific session)
GBSM commitTransaction (to commit the current session)
or, in the GemStone Launcher, select the session and click on the Commit button; or in any GBS browser, use the Session menu item Commit Transaction, or the toolbar icon.
When the commit succeeds, the method returns true. Successfully committing a transaction has two effects:
A commit request can be unsuccessful in two ways:
In order to commit, the session must be operating within a transaction. An attempt to commit while outside a transaction raises an exception.
When a session aborts its transaction, it discards any uncommitted changes to persistent objects and refreshes its view of the shared object repository. Despite the terminology, a session need not be operating inside a transaction in order to abort. To abort, send the message:
aGbsSession abortTransaction (to abort a specific session)
GBSM abortTransaction (to abort the current session)
or, in the GemStone Launcher, select the session and click on the Abort button; or in any GBS browser, use the Session menu item Abort Transaction, or the toolbar icon.
You can use the GemBuilder method GbsSession >> hasConflicts to determine if any concurrency conflicts exist that would cause a subsequent commit operation to fail. It returns false if it finds no conflicts with other concurrent transactions, true otherwise. You can then determine how best to proceed.
If an attempt to commit fails because of a concurrency conflict, the commitTransaction method returns false.
Following a commit failure, the client’s view of persistent objects may differ from their precommit state:
Following a commit failure, your session must refresh its view of the repository by aborting the current transaction. The uncommitted transaction remains in effect so you can save some of its contents, if necessary, before aborting.
A common strategy for handling such a failure is to abort, then reinvoke the method in which the commit occurred. Depending on your application, you may simply choose to discard the transaction and move on, or you may choose to remedy the specific transaction conflict that caused the failure, then initiate a new transaction and commit.
If you want to know why a transaction failed to commit, you can send the message:
aGbsSession transactionConflicts
This expression returns a symbol dictionary whose keys indicate the kind of conflict detected and whose values identify the objects that incurred each kind of conflict. (See Managing Concurrent Transactions for more discussion of the kinds of conflicts that can arise.)
A session must be inside a transaction in order to commit. While operating within a transaction, every change the session makes and every new object it creates can be a candidate for propagation to the shared repository. GemBuilder monitors the operations that occur within the transaction, gathering all the necessary information required to prepare the transaction to be committed.
For efficiency, an application may configure a session to operate outside a transaction. When operating outside a transaction, a session can view the repository, browse the objects it contains, and even make computations based upon their values, but it cannot commit any new or changed GemStone server objects. When a session is operating outside a transaction, the Stone may request that the session abort. A session operating outside a transaction can, at any time, begin a transaction.
No session is overhead-free: even a session operating outside a transaction uses GemStone resources to manage its objects and its view of the repository. For best system performance, all sessions, even those running outside a transaction, must periodically refresh their views of the repository by committing or aborting.
Table 5.1 shows GbsSession methods that support running outside of a GemStone transaction:
Executes aBlock when a signal to abort is received (see below). |
To begin a transaction, send the message:
aGbsSession beginTransaction (to begin a transaction for a specific session)
GBSM beginTransaction (to begin a transaction for the current session)
or, in the GemStone Launcher, select the session and click on the Begin button; or in any GBS browser, use the Session menu item Begin Transaction, or the toolbar icon.
This message discards any local modifications, gives you a fresh view of the repository, and starts a transaction. When you abort or successfully commit this new transaction, you will again be outside of a transaction until you either explicitly begin a new one or change transaction modes.
If you are not currently in a transaction, but still want a fresh view of the repository, you can send the message aGbsSession abortTransaction. This discards modifications to your current view of the repository and gives you a fresh view, but does not start a new transaction when you are in manual transaction mode.
When you are in a transaction, GemStone waits until you commit or abort to reclaim storage for objects that have been made obsolete by your changes. When you are running outside of a transaction, however, you are implicitly giving GemStone permission to send your Gem session a signal requesting that you abort your current view so that GemStone can reclaim storage when necessary. When this happens, you must respond within the time period specified in the STN_GEM_ABORT_TIMEOUT parameter in the Stone’s configuration file. If you do not, GemStone either terminates the Gem or forces an abort, depending on the value of the related configuration parameter STN_GEM_LOSTOT_TIMEOUT. The Stone forces an abort by sending your session an abortErrLostOtRoot signal, which means that your view of the repository was lost, and any objects that your application had been holding may no longer be valid. When your application receives abortErrLostOtRoot, the application must log out of GemStone and log back in, thus rereading all of its data in order to resynchronize its snapshot of the current state of the GemStone repository.
You can avoid abortErrLostOtRoot and control what happens when you receive a signal to abort with the signaledAbortAction: aBlock message. For example:
aGbsSession signaledAbortAction:
[aGbsSession abortTransaction].
This causes your GemBuilder session to abort when it receives a signal to abort.
A GemBuilder session always initiates a transaction when it logs in. After login, the session can operate in either of two transaction modes: automatic or manual.
In automatic transaction mode, committing or aborting a transaction automatically starts a new transaction. This is GemBuilder’s default transaction mode: in this mode, the session operates within a transaction the entire time it is logged into GemStone.
However, being in a transaction incurs certain costs related to maintaining a consistent view of the repository at all times for all sessions. Objects that the repository contained when you started the transaction are preserved in your view, even if you are not using them and other users' actions have rendered them meaningless or obsolete.
Depending upon the characteristics of your particular installation (such as the number of users, the frequency of transactions, and the extent of object sharing), this burden can be trivial or significant. If it is significant at your site, you may want to reduce overhead by using sessions that run outside transactions, so that the Stone can signal transactions to abort when necessary. To run outside a transaction, a session must switch to manual transaction mode.
In manual transaction mode, the session remains outside a transaction until you begin a transaction. When you change the transaction mode from automatic (its initial setting) to manual, the current transaction is aborted and the session is left outside a transaction. In manual transaction mode, a transaction begins only as a result of an explicit request. When you abort or commit successfully, the session remains outside a transaction until a new transaction is initiated.
To begin a transaction, send the message
aGbsSession beginTransaction
or select the Begin button on the GemStone Launcher.
A new transaction always begins with an abort to refresh the session’s private view of the repository. Local objects that customarily survive an abort operation, such as temporary results you have computed while outside a transaction, can be carried into the new transaction where they can be committed, subject to the usual constraints of conflict-checking. If you begin a new transaction while already inside a transaction, the effect is the same as an abort.
In manual transaction mode, as in automatic mode, an unsuccessful commit leaves the session in the current transaction until you take steps to end the transaction by aborting.
You should use automatic transaction mode if the work you are doing requires committing to the repository frequently, because you can make permanent changes to the repository only when you are in a transaction.
Use manual transaction mode if the work you are doing requires looking at objects in the repository, but only seldom requires committing changes to the repository. You will have to start a transaction manually before you can commit your changes to the repository, but the system will be able to run with less overhead.
To find out if you are currently in a transaction, execute aGbsSession inTransaction. This returns true if you are in a transaction and false if you are not.
To change from manual to automatic transaction mode, execute the expression:
aGbsSession transactionMode: #autoBegin
This message automatically aborts the transaction, if any, changes the transaction mode, and starts a new transaction.
To change from automatic to manual transaction mode, execute the expression:
aGbsSession transactionMode: #manualBegin
This message automatically aborts the current transaction and changes the transaction mode to manual. It does not start a new transaction, but it does provide a fresh view of the repository.
GemBuilder has the ability to enable Auto-commit. This is specific to GemBuilder, it is not a GemStone server mode. Auto-commit is only available for sessions in automatic transaction mode.
In Auto-commit mode, every code change made in the browsers is automatically committed to the repository, which also starts a new transaction. Changes made in inspectors or debuggers do not trigger the commit, although any changes that have been made are included in the next commit.
Auto-commit avoids the risk of losing work, but each method change updates your transactional view of the server. This may result in unexpected changes in a multi-user system in which other users may commit changes to server objects you are using.
When you tell GemStone to commit your transaction, it checks to see if doing so presents a conflict with the activities of any other users.
1. It checks to see whether other concurrent sessions have committed transactions of their own, modifying an object that you have also modified during your transaction. If they have, then the resulting modified objects can be inconsistent with each other.
2. It may check to see whether other concurrent sessions have committed transactions of their own, modifying an object that you have read during your transaction, while at the same time you have modified an object that the other session has read.
3. It checks for locks set by other sessions that indicate the intention to modify objects that you have read or to read or write objects you have modified in your view.
If it finds no such conflicts, GemStone commits the transaction, and your work becomes part of the permanent, shared repository. Your view of the repository is refreshed and any new or modified objects that other users have recently committed become visible in any dictionaries that you share with them.
For details about read and write operations, optimistic and pessimistic concurrency control, and other general information about GemStone transactions, refer to the “Transactions and Concurrency Control” chapter of the GemStone/S 64 Bit Programming Guide.
GemBuilder provides locking protocol that allows application developers to write client Smalltalk code to lock objects and specify client Smalltalk code to be executed if locking fails.
A GbsSession is the receiver of all lock requests. Locks can be requested on a single object or on a collection of objects. Single lock requests are made with the following statements:
aGbsSession readLock: anObject.
aGbsSession writeLock: anObject.
The above messages request a particular type of lock on anObject. Lock types are described in the GemStone Programming Guide. If the lock is granted, the method returns the receiver. If you don’t have the proper authorization, or if another session already has a conflicting lock, an error will be generated.
When you request a lock, an error will be generated if another session has committed a change to anObject since the beginning of the current transaction. In this case, the lock is granted despite the error, but it is seen as “dirty.” A session holding a dirty lock cannot commit its transaction, but must abort to obtain an up-to-date value for anObject. The lock will remain, however, after the transaction is aborted.
Another version of the lock request allows these possible error conditions to be detected and acted on.
aGbsSession readLock: anObject ifDenied: block1 ifChanged: block2
aGbsSession writeLock: anObject ifDenied: block1 ifChanged: block2
If another session has committed a change to anObject since the beginning of the current transaction, the lock is granted but dirty, and the method returns the value of the zero-argument block block2.
The following statements request locks on each element in the three different collections.
aGbsSession readLockAll: aCollection.
aGbsSession writeLockAll: aCollection.
The following statements request locks on a collection, acquiring locks on as many objects in aCollection as possible. If you do not have the proper authorization for any object in the collection, an error is generated and no locks are granted.
aGbsSession readLockAll: aCollection ifIncomplete: block1
aGbsSession writeLockAll: aCollection ifIncomplete: block1
Example 5.1 shows how error handling might be implemented for the collection locking methods:
getWriteLocksOn:aCollection
"This method attempts to set write locks on the elements
of a Collection."
aGbsSession
writeLockAll: aCollection
ifIncomplete: [ :result |
(result at: 1)isEmpty ifFalse:
[ self handleDenialOn: denied].
(result at: 2)isEmpty ifFalse:
[aGbsSession abortTransaction].
(result at: 3)isEmpty ifFalse:
[aGbsSession abortTransaction].
].
Once you lock an object, it normally remains locked until you either log out or explicitly remove the lock; unless you specify otherwise, locks persist through aborts and commits. In general, you should remove a lock on an object when you have used the object, committed the resulting values to the repository, and no longer anticipate a need to maintain control of the object.
The following methods are used to remove specific locks.
aGbsSession removeLock: anObject.
aGbsSession removeLockAll: aCollection.
aGbsSession removeLocksForSession.
The following methods answer various lock inquiries:
aGbsSession sessionLocks.
aGbsSession systemLocks.
aGbsSession lockOwners: anObject.
aGbsSession lockKind: anObject.
The following statements add a locked object or the locked elements of a collection to the set of objects whose locks are to be released upon the next commit or abort:
aGbsSession addToCommitReleaseLocksSet: aLockedObject
aGbsSession addToCommitOrAbortReleaseLocksSet: aLockedObject
aGbsSession addAllToCommitReleaseLocksSet: aLockedCollection
aGbsSession addAllToCommitOrAbortReleaseLocksSet: aLockedCollection
If you add an object to one of these sets and then request a fresh lock on it, the object is removed from the set.
You can remove objects from these sets without removing the lock on the object. The following statements show how to do this:
aGbsSession removeFromCommitReleaseLocksSet: aLockedObject
aGbsSession removeFromCommitOrAbortReleaseLocksSet: aLockedObject
aGbsSession removeAllFromCommitReleaseLocksSet: aLockedCollection
aGbsSession removeAllFromCommitOrAbortReleaseLocksSet: aLockedCollection
The following GemStone Smalltalk statements remove all objects from the set of objects whose locks are to be released upon the next commit or abort. These methods are executed using GS-Do it:
System clearCommitReleaseLocksSet
System clearCommitOrAbortReleaseLocksSet
The statement aGbsSession commitAndReleaseLocks attempts to commit the current transaction, and clears all locks for the session if the transaction was successfully committed.
At times GemStone will perceive a conflict when two users are accessing the same object, when what the users are doing actually presents no problem. For example, GemStone may perceive a write/write conflict when two users are simultaneously trying to add an object to a Bag that they both have access to because this is seen as modifying the Bag.
GemStone provides some reduced-conflict classes that can be used instead of their regular counterparts in applications that might otherwise experience too many unnecessary conflicts. For details, refer to the “Transactions and Concurrency Control” chapter of the GemStone/S 64 Bit Programming Guide.
A notifier is an optional signal that is activated when an object’s committed state changes. Notifiers allow sessions to monitor the status of designated shared application objects. A program that monitors stock prices, for example, could use notifiers to detect changes in the prices of certain stocks.
In order to be notified that an object has changed, a session must register that object with the system by adding it to the session’s notify set.
Notify sets persist through transactions, living as long as the GemStone session in which they were created. When the session ends, the notify set is no longer in effect. If you need it for your next session, you must recreate it. However, you need not recreate it from one transaction to the next.
Class GbsSession provides the following two methods for adding objects to notifySets:
addToNotifySet:
adds one object to the notify set
addAllToNotifySet:
adds the contents of a collection to the notify set
When an object in the notify set appears in the write set of any committing transaction, the system evaluates a client Smalltalk block, sending a collection of the changed objects as an argument to the block. By examining the argument, the session can determine exactly which objects triggered the signal. (The block must have been previously defined by sending notificationAction: to the session, with the block as the argument.)
Because these events are not initiated by your session but cause code to run within your session, this code is run asynchronously in a separate Smalltalk process. Depending on what else is occurring in your application at that time, using this feature might introduce multi-threading into your application, requiring you to take some additional precautions. (See Multiprocess Applications.)
Example 5.2 demonstrates notification in GemBuilder.
"First, set up notifying objects and notification action"
| notifier |
GBSM currentSession abortTransaction; clearNotifySet.
notifier := Array new: 1.
GBSM currentSession at: #Notifier put: notifier.
GBSM currentSession commitTransaction.
GBSM currentSession addToNotifySet: notifier.
GBSM currentSession notificationAction: [ :objs |
Transcript cr; show: 'Notification received' ]
"Now, from any session logged into the same stone with visibility
to the object 'notifier' - to initiate notification"
GBSM currentSession abortTransaction;
evaluate: 'Notifier at: 1 put: Object new';
commitTransaction
Sessions can send general purpose signals to other GemStone sessions, allowing the transmission of the sender’s session, a numerical signal value, and an associated message string.
One Gem can handle a signal from another using the method GbsSession >> sessionSignalAction: aBlock, where aBlock is a one-argument block that will be passed a forwarder to the instance of GsInterSessionSignal that was received. From the GsInterSessionSignal instance, you can extract the session, signal value, and string.
One GemStone session sends a signal to another with:
aGbsSession sendSignal: aSignal to: aSessionId withMessage: aString
"First, set up the signal-receiving action"
GBSM currentSession sessionSignalAction: [ :giss |
nil gbsMessenger
comment: 'Signal %1 received from session %2: %3.'
with: giss signal
with: giss session
with: giss message.
].
"Now, from any session logged into the same Stone, send a signal.
(This example uses the same session)"
GBSM currentSession
sendSignal: 15
to: (GBSM evaluate: 'GsCurrentSession currentSession serialNumber')
withMessage: 'This is the signal'.
If the signal is received during GemStone execution, the signal is processed and execution continues. If aBlock is nil, any previously installed signal action is deinstalled.
NOTE
The method sessionSignalAction: and the mechanism described above supersede the old mechanism that used the method gemSignalAction:. Do not use both this method and gemSignalAction: during the same session; only the last defined signal handler will remain in effect.
See the chapter entitled “Error-handling” in your GemStone Programming Guide for details on using the error mechanism for change notification.
For each session, there is a background thread that detects events from the server such as sigAbort, lostOTroot, gem to gem signals, and changed object notifications, and other events that are handled internally. If a non-fatal error occurs in processing these events, by default a walkback is opened.
To avoid an end-user experiencing a walkback, you may set a handler block for an unexpected error in this event detection, using the method:
GbsSession >> eventDetectorErrorHandler: aOneArgBlock
If the eventDetectorErrorHandler is set, and if the exception is not already handled by another handler that is set up for the application, this handler block will be executed for the exception caught by the event detection thread.