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.
Transactions
explains the transaction model, committing, and aborting.
Transaction Modes and Auto-Commit
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.
Gem-to-Gem Notification
describes the mechanisms for inter-gem communications.
Asynchronous Event Error Handling
explains how to handle errors asynchronously.
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.
Sessions may also be out of 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. A session operating outside a transaction can, at any time, begin a transaction.
The transaction mode controls if the session is always in a transaction, or must explicitly begin a transaction. There are three transaction modes: automatic transaction mode, manual transaction mode, and transactionless. In automatic transaction mode, a session is always in transaction—a new transaction is started whenever a transaction completes. In manual and transactionless mode, a transaction must be explicitly started.
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.
Before the application starts collecting object changes that it intends to committed, it must begin a transaction. In automatic transaction mode, this occurs automatically following login, commit, or abort, but a begin transaction must be explicitly done in other transaction modes.
To begin a transaction from the client, send the message:
aGbsSession beginTransaction (to begin a transaction on a specific session)
GBSM beginTransaction (to begin a transaction on 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.
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, and must be kept track of.
To avoid tying up server resources, an application may configure a session to operate outside a transaction. When a session is operating outside a transaction, the Stone may request that the session abort, to free up resources.
When you are in a transaction, GemStone must keep references to every object that was in your view when you started that transaction, including objects that you are actually not using and that are no longer referenced by any other sessions. These obsolete objects cannot be reclaimed until you commit or abort.
When you are running outside of a transaction, 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 that storage. 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 GemStone server 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 should generally 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 automatically abort when it receives a signal to abort.
The GemStone server has three transaction modes, automatic, manual begin, and transactionless. Transactionless is intended for idle sessions, and is not intended for use when actively working, since the view may be updated at any time. In combination with GemBuilder’s auto-commit, there are three modes you may use when working in GemBuilder:
On login, the GemBuilder session transaction mode depends on how this is configured in the GemStone server; by default, automatic transaction mode. The mode can be changed after login.
GemBuilder by default has auto-commit off. This can be enabled after login using the toolbar toggle, or menu items.
In automatic transaction mode, committing or aborting a transaction automatically starts a new transaction. This is the default, although it can be configured on the GemStone server. In automatic transaction mode, the session operates within a transaction the entire time it is logged into GemStone.
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, and must be maintained in the shared repository.
In manual transaction mode, the session remains outside a transaction until you choose to begin a transaction. When you abort, or commit successfully, the session goes out of transaction and remains outside a transaction until another begin transaction. You may abort in this mode, which updates your view of the repository, but you may not commit.
When you change the transaction mode from automatic to manual, the current transaction is aborted and the session is left outside a transaction.
When a new transaction is begun, it automatically performs an abort to refresh the session’s private view of the repository. Objects that have never been committed are not affected by abort, and can be carried into the new transaction where they can be committed (subject to the usual constraints of conflict-checking).
In manual transaction mode, as in automatic mode, an unsuccessful commit leaves the session in the same transaction until you end the transaction by aborting.
Transactionless mode is similar to manual transaction mode, but in transactionless mode, the session does not maintain a consistent view of the repository. Transactionless sessions may be automatically aborted. Since active work depends on a stable view of the repository, transactionless mode is intended for idle sessions, and there is minimal support in GemBuilder.
In addition to GemStone server transaction modes, you may enable auto-commit in GemBuilder. Auto-commit is only available for sessions in automatic transaction mode. Auto-commit applies specifically to code changes, not to all changes in object state.
To enable auto-commit, use the toolbar option on any code browser, or a code browser’s Session menu item Auto-Commit; it is also available in the Launcher Session menu item Auto-Commit in Browsers.
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 the same server objects you are using.
During code development, where you may unexpectedly error and lose your session, running in automatic transaction mode with auto-commit ensures that code changes are never lost. However, any code changes you do make are permanent.
If you will be committing changes, but do not want each individual code change to be immediately committed, automatic transaction mode without auto-commit avoids the need to start a transaction before making changes.
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.
Table 5.1 shows GbsSession methods that support GemStone transactions:
Aborts the current transaction, updates the view with any changed server objects, and begins a transaction. |
|
Commits the current transaction, if possible, and updates the view with any changed server objects. If in automatic transaction mode, begins a new transaction. |
|
Aborts the current transaction and updates the view with any changed server objects. |
|
Executes aBlock when a signal to abort is received (see Being Signaled to Abort by the Stone). |
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 more details on transaction management, see 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.
This will write a message to the transcript."
GBSM currentSession sessionSignalAction: [ :giss |
nil gbsMessenger
comment: 'Signal %1 received from session %2: %3.'
with: giss signal
with: giss session
with: giss message.
].
"Now, send the signal.
This example sends a signal to 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.