The following files are supplied (in addition to this document):
· tenacity.dat - the library containing Tenacity - place this in your VisualAge Import directory
· readme.txt - the first level instructions provided with Tenacity and Tenacity Example Application.
Copy the files to their respective locations as noted above.
In the VA organiser, select the Applications menu - Import/Export and select Import Applications.
Direct the file browser to your import directory and select toten6-0_1-0-0.dat.
In your System Transcript, select the Smalltalk Tools menu and the select Browse Application Editions
Select TenacityCommonApp and load the edition shown.
Select TenacityExampleApp and load the edition shown.
Tenacity comprises IBM Smalltalk code to provide database facilities. Tenacity is a true object oriented database and stores IBM Smalltalk objects and retrieves them under full atomic transaction control.
Tenacity is written, 100%, in IBM Smalltalk. This means that it is compatible both with IBM Smalltalk and IBM VisualAge Smalltalk. As no code other than that supplied by IBM is used, Tenacity should be compatible with all Smalltalk/VisualAge supported operating systems.
Due to lack of hardware, Tenacity has only been tested under Windows of all flavours and OS/2 Warp. Thus, it cannot be guaranteed that it will operate fully without problems on the other VA supported operating systems. However, if any incompatibilities are discovered and reported to Totally Objects, we will do our best to resolve them or provide a full refund, provided that we are informed within 30 days of providing the software to you.
Tenacity is a persistent object database add on for IBM Smalltalk and IBM VisualAge Smalltalk. It provides a simple means of transforming any object within memory into its persistent equivalent, as follows.
anObject:=’Test String’ asPersistent.
Like any other object, it must be referenced otherwise it will be garbage collected.
A Tenacity database consists of a single root object which contains a range of dictionaries, or other Collection type objects, containing persistent objects.
This mechanism permits simple or complex database structures to be built using standard Smalltalk code. In fact, Tenacity requires very few specialised methods for its operation. Almost every action is carried out largely with standard Smalltalk. For more details on the extra methods and their results look at Tenacity Methods, later.
Tenacity comes with a range of classes and some extensions to existing classes. Each of these has a specialised task. Only the methods categorised within TenacityAPI should be used. All other categories and classes supplied are private. The methods provided within them should not be used.
This is the main database management class. All of the methods are directed at the database - not at individual objects. You will use this class to create a database, connect to a database and add and manage database slices.
Persistent objects, in memory, are represented by instances of class TenacityPP. Each TenacityPP knows how to recover its own object from the database.
Extensions are provided to Object that support Tenacity directly. These include the #actualPrintString method, which only implements #printString for non-persistent objects for compatibility, methods to make existing objects persistent and an extension to the comparison methods available (this is discussed later).
To avoid having to manage transactions directly, a new range of block methods are provided. These control the form of transaction to be used and also provide automatic re-try mechanisms.
The following classes are also supplied. However, none of these have any public methods at all and thus should not be used by you in your code development. If you find that you could usefully utilise additional facilities which may require the use of these classes, please discuss it with us first.
Please note that the beta version contains some public methods that should not be available so take care and use those methods we tell you about here. In any case, if you read your licence, you will see that you should not be writing end user code with the beta.
Tenacity uses the technique of proxies to enable Smalltalk developers to create persistent objects in their applications. If one persistent object wishes to reference another, as one of its instance variables say, then a proxy is used instead. The proxy contains enough information to recover the referenced object from the database. If a message is sent to the proxy, instead of answering ‘Does not understand’, it gets the reference object from the database and sends the message to it.
The diagram below shows six persistent objects in a database.
If the persistent object with id = 2 is read from the database then its six instance variables are also read: 2 Strings, 1 Date, 2 Proxies and 1 Set containing a Proxy. NO OTHER PERSISTENT OBJECTS HAVE BEEN READ. If this Person is sent a message asking it for his father a Proxy will be answered. If that Proxy is sent a message asking it for its name then the persistent object with id = 0 is read from the database and it is sent the same message answering ‘Mr. Smith’.
A proxy results from sending #asPersistent to any object. (Due to constraints with the Object Dumper, you cannot make a SortedCollection persistent.)
The root of your database is a persistent object that can be accessed without a proxy. It should be used as a hook to access other objects. Usually this will contain a dictionary of proxies to collections.
Slices are partitions in a database and can be stored in different directories or on different disk drives. The slice in which to store a persistent object is specified by sending it #asPersistent: with the name of the slice as the argument. Objects made persistent using the #asPersistent message are put into a default slice which is specified by the developer. Slices are divided into divisions. The number of divisions should be chosen to match the estimated maximum size of your database.
All messages that cause a read or write to a database should be performed within a transaction. The two most common types of transaction are Read transactions and ReadWrite transactions. In all types of transaction, if any persistent object is read from the database, then it will be kept in memory until the transaction finishes. In ReadWrite transactions, all the read persistent objects are written back to the database. In read transactions, all objects are discarded at the completion of the transaction, whether changed or not.
Messages are performed during a transactions by putting them inside atomic blocks. This is best explained using an example. The code below sends messages to persistent objects during a read transaction.
Send messages to persistent objects
Similarly a ReadWrite transactions can be used by sending #atomicReadWrite (or just #atomic) to the block.
Before the code inside an atomic block is executed the transaction will to lock the database. If this fails then the transaction will retry a specified number of times. If the database cannot still be locked at the end of the retires, an exception will be thrown(see later). The lock applied to the database depends upon the form of transaction used.The following table shows the transactions that are available, the locks they check for and the locks they apply.
For most read operations
For operations when the read operations must be consistent throughout the transaction
For most write operations. All objects read during these transaction will be written to the database.
Read + Write
For writing when read transactions should not start. For example, during a purging or end-of-day process. All objects read during these transaction will be written to the database.
Only nominated objects read during these transaction will be written to the database. Persistent objects can be nominated by sending them the message #nominateForWriting
Read transactions check for a read lock only so that they can be locked out by an exclusiveReadWrite. In all other circumstances, the read lock check will not stop a read from happening.
Sometimes bugs in your code during development can cause the database to be left locked when you do not want it to be. To avoid these problems, Tencacity has been configured to only lock the database at runtime. Use the Tenacity class method #lockDuringDevelopment: with a Boolean as the argument to have locks applied.
We mentioned earlier that you can make transactions by using #atomicRead, #atomicReadWrite or #atomic, but you can also use #atomicExclusiveRead, #atomicExclusiveReadWrite and #atomicNominatedReadWrite for the appropriate transaction type. The transactions can also be created with the following messages #atomicRead:, #atomicExclusiveRead:, #atomicReadWrite: (or #atomic:), #atomicExclusiveReadWrite: and #atomicNominatedReadWrite:. With these message the argument is the number of attempts the transaction will have at applying the locks before giving up. If the messages without an argument are used then a default number of attempts are tried. This default can be got and set by sending #defaultNumberOfAttempts and #defaultNumberOfAttempts: to the TenacityTransaction class. Varying PC performance will require varying numbers of retries. A 33MHz 486 will require less retries than a 200MHz MMX Pentium.
Your applications can only be connected to one database at a time. The currently connected database can be accessed by sending the message #current to the class Tenacity.
Your database is created by sending the message #createDatabase:prefix: to the class Tenacity. The first argument is the path of the directory that will contain the database management file (this will contain the root object and details of the names and locations of the slices). The second argument is a string that will be used as a prefix to all the files containing persistents. You application will now automatically be connected to this new database.
Slices are created by sending the messages #addSlice:directory: or #addDefaultSlice:directory: to the current database. The arguments for these messages are the number for the slice and a string containing the slices path. There must be at least one slice.
The next step is to create the root. This is an object that enables you to access other objects in your database. It is set using
The example below gives shows how a typical database might be created.
“Create the database”
Tenacity createDatabase: 'c:\Finance Database' prefix: 'FIN'.
| dict |
“Create the slices”
addSlice: 0 directory: 'c:\Finance Database\Customers';
addSlice: 1 directory: 'o:\Network Folder';
addDefaultSlice: 2 directory: 'o:\Finance Database\Default Folder'.
“Create the Root”
dict := Dictionary new.
at: #foreignCustomers put: (Dictionary new asPersistent: 0);
at: #homeCustomers put: (Dictionary new asPersistent: 0);
at: #users put: (Set new
add: (FinanceApplicationUser new username: 'administrator'; password: 'finance'; asPersistent: 1);
at: #letters put: (Set new asPersistent)
at: #creationDate put: (Date today);
Tenacity current root: dict.
The Root is not persistent as it is never represented by a proxy. Items added to the root also do not need to be persistent (see #creationDate above) but this means that they will always be loaded into memory in full at each access. There will not be a proxy for any non-persistent object in the root.
Note that not all of the items in the root dictionary are persistent. When this root dictionary is read from the database none of the other Sets and Dictionaries are read unless and until they are needed.
To detect if a database exists in a particular directory send the class Tenacity the message #directoryContainsDatabase: with a path as the argument. If a database exists then you can connect to it by sending the class Tenacity the message #connectToDatabase: sending the same path as the argument.
To disconnect from the current database use the message #disconnect. I.e. do the following expression ‘Tenacity current disconnect’
The following exceptions may be thrown while doing a Tenacity operation. The child status is shown as any exception catching will also cath any child exceptions.
A high level abstract exception. This can be got by sending #exTenacity to the Tenacity class.
Thrown whenever a transaction cannot lock or unlock the database for a reason other than the database is locked or unlocked already. This can be got by sending #exTransaction to the Tenacity class.
Thrown if Tenacity fails to connect to a database or if a file error prevents Tenacity from reading an object. This can be got by sending #exObjectLoad to the Tenacity class.
Thrown if a file error prevents Tenacity from writing an object or removing a slice. This can be got by sending #exObjectSave to the Tenacity class.
Thrown if a transaction fails to start because the database is locked. The transaction will retry the specified number of times before throwing this exception. This can be got by sending #exLocking to the Tenacity class.
Thrown if an invalid slice is referenced of an unacceptable slice number used (slices must be number in the range 0 to 255 inclusive). This can be got by sending #exSlice to the Tenacity class.
Thrown if #persistentProxy is sent to an object that does not implement #tenacityMyProxy and #tenacityMyProxy: (as described later) outside a transaction. This can be got by sending #exProxy to the Tenacity class.
Thrown when trying to connect to a database that cannot be connected to with this release of Tenacity. This can be got by sending #exFormat to the Tenacity class.
For your convenience the following methods have been supplied for making exception catching easier. #atomicReadWhenExTransaction: (which takes a one argument block as its argument) and #atomicRead:whenExTransaction: (which takes the number of attempts required and a one argument block as its arguments). Similar methods exist for the other types of transaction.
If any exception (other than ExHalt) is thrown but not caught inside the atomic block, then no changes made to persistent objects will be written back to the database. If an ExHalt is thrown but not resumed then it is necessary to clear the current transaction (in your Transcript window) by sending #clearCurrent to the Tenacity class.
Warning: If the code is changed in the debugger following a Halt, or otherwise, a new process is always started. As a transaction only holds within a single process, the ensuing database activity will fail due to a lack of a transaction. You should not resume within the debugger if you change any code whilst inside a transaction.
As objects are changed or deleted, the original object is just de-referenced, in the same way as happens with objects in memory. This will result in the database growing at a rate that is not commensurate with the objects held in it.
Garbage collection within memory is automatic. That is not the case with the database version. You must explicitly run it. To do so send #garbageCollect or #garbageCollectWithReadLock to the Tenacity class. No transactions are needed.
With a database of any size, this operation can take an extended amount of time. You should only carry our a garbage collection when it will not interfere with the normal running of the database.
In addition, there must be no others using the database at that time so the garbage collection takes place inside a transaction. It will lock the database for the whole time that it is running.
It is not possible to report progress as the operation does not know how much still needs to be done at any one time.
Beware when sending self as an argument in a message sent to other objects. The object sent is just a copy of the one in the database and not the proxy. Any future messages sent to it will not be sent via the proxy. When the object’s proxy is required send #persistenProxy to the object and use the result of this message as the argument instead of self. For example if too persistent People objects wish to have a reference to each other because they are brothers then the code would like something like this:
self brothers add: aPerson.
aPerson brothers add: (self persistentProxy)
This works inside a transaction because Tenacity can look-up the objects that have been read during the transaction and deduce the proxy. Outside of a transaction how ever Tenacity discovers the proxy by sending the object the message #tenacityMyProxy. This means that if you wish to use the #persistentProxy message outside a transaction your object must understand #tenacityMyProxy: (which must store the argument) and #tenacityMyProxy which must answer the stored object. Failure to do this will result in the exception ExProxy being thrown.
Proxies use the message #printString when displaying themselves in the debug window. The message will not be sent onto the object in the database, so the answered string will. You should use alternative methods such as Integer’s #printStringRadix:showRadix:. We have provided a general #actualPrintString method which will send a #printString message to the object in the database which does get around this problem.
There will be times when messages are sent to your persistent objects and you will not be able to use a transaction. This happens, for instance, when widgets displaying your objects are resized. When this happens, Tenacity will automatically create a read transaction for you. Some methods have been provided to do extra caching so as to speed up the refreshing of widgets in these circumstances. These methods are still being tested so should be used with caution. A brief overview is given below.
This can be sent to any persistent object during a transaction and will keep a copy of the object in memory. Any messages sent to a proxy outside a transaction will be bounced onto the cached object. The argument should be an object such that when it is garbage collected (by the memory garbage collector) the cache will be deleted. If this message is sent to a proxy more than once with a different argument each time then the cache will not be destroyed until all the arguments have been garbage collected.
Send #tenacityCacheFor: to each item it contains.
Does nothing - provided to allow for the sending of the message to non-persistent objects.
It is intended that an OrderedCollection of persistent objects appearing in an AbtContainerDetailsView (say) should be sent the message #tenacityCacheAllFor: with the AbtContainerDetailsView itself as the argument. When the window containing the view is no longer needed it should be destroyed thus enabling the cache to be cleared during the next garbage collection.
Never use ^ within an atomic block; the transaction will not be cleared. If you do need to bomb-out or roll-back throw your own exception and catch it outside the block.
Read transactions can be nested inside other Read transactions. No other nesting of atomic blocks is allowed. This means that you should keep your atomic blocks in high level methods. For example in the method triggered by an event. With this in mind, keep the amount of processing that happens inside the block to a minimum to avoid locking other people from the database. Be careful when forking new processes. A transaction cannot start in one process if a transaction in another has not finished.
Sending #lastModified to a persistent object will answer an array containing the date and time that the object was last written to the database. No transaction is needed. This is extremely useful if you want to actively detect if changes have been made to an object since you last read it.
To avoid problems of the database being locked accidentally during development, locks are applied only in a runtime image. To change this use the Tenacity class method #lockDuringDevelopment: giving a Boolean as an argument. You should make sure that you have removed all halts from within your transactions before turning on locking or building your runtime image.
At the of Tenacity is the ObjectSwapper (with each persistent object having a file of its own). This means that there are limits to the size of individual objects that can be written and also the number of objects reference by persistent objects. See “IBM Smalltalk User’s Guide 22.214.171.124”. Although the ObjectLoader and ObjectDumper are not accessible to developers, Tenacity>>#addMutatedClassNamed:newClass:, Tenacity>>#removeMutatedClassNamed: and Tenacity>>unlinkInstancesOfClasses:, Tenacity>>unlinkInstVars:fromClasses: and Tenacity>>unlinkObjects:have been provided to give you the interface that you need.
Avoid using the names of methods in TenacityPP for any objects that will be made persistent, except for standard Smalltalk methods (such as #hash and #=).
If you have two objects that reference the same object (say, using an instance variable) and these objects are made persistent, then upon getting the objects back from the database, the two objects will now reference copies of the same object. When this is undesirable then the reference object must be made persistent in its own right.
Example 1 - Inspecting the following gives false
[ | mother child1 child2 |
mother := Person new.
child1 := Person new mother: mother.
child2 := Person new mother: mother.
(Tenacity current root)
at: #child1 put: (child1 asPersistent);
at: #child2 put: (child2 asPersistent).
((Tenacity current root at:#child1) mother) ==
((Tenacity current root at:#child2) mother)
Example 2 - Inspecting the following gives false
[ | mother child1 child2 |
mother := Person new asPersistent.
child1 := Person new mother: mother.
child2 := Person new mother: mother.
(Tenacity current root)
at: #child1 put: (child1 asPersistent);
at: #child2 put: (child2 asPersistent).
((Tenacity current root at:#child1) mother) ==
((Tenacity current root at:#child2) mother)
atomic or atomicReadWrite
Creates a ReadWrite transaction with the default number of attempts (4).
atomic: or atomicReadWrite:
As #atomic but the number of attempts is given.
atomic:whenExTransaction: or atomicReadWrite:whenExTransaction:
As #atomic: but expects a single argument block to handle exceptions.
atomicWhenExTransaction: or atomicReadWriteWhenExTransaction:
As #atomic but expects a single argument block to handle exceptions.
Creates an ExclusiveRead transaction with the default number of attempts (4).
As #atomicExclusiveRead but the number of attempts is given.
As #atomicExclusiveRead: but expects a single argument block to handle exceptions.
As #atomicExclusiveRead but expects a single argument block to handle exceptions.
Creates an ExclusiveReadWrite transaction with the default number of attempts (4).
As #atomicExclusiveReadWrite but the number of attempts is given.
As #atomicExclusiveReadWrite: but expects a single argument block to handle exceptions.
As #atomicExclusiveReadWrite but expects a single argument block to handle exceptions.
Creates a NominatedReadWrite transaction with the default number of attempts (4).
As #atomicNominatedReadWrite but the number of attempts is given.
As #atomicNominatedReadWrite: but expects a single argument block to handle exceptions.
As #atomicNominatedReadWrite but expects a single argument block to handle exceptions.
Creates a Read transaction with the default number of attempts (4).
As #atomicRead but the number of attempts is given.
As #atomicRead: but expects a single argument block to handle exceptions.
As #atomicRead but expects a single argument block to handle exceptions.
To be used inside a transaction. Any objects got from a database during this block are not cached for the duration of the transaction. Any further messages sent to the objects during the database will cause them to be read again. If you were to read many objects from your database during one transaction you may run short of memory. This method can keep the amount of memory need much smaller. Note that if you are reading a large persistent collection and wish to send a message to each object in it. The following code should be used as a guide.
| largeSet total |
largeSet := Tenacity current root at: #theLargeSet.
largeSet yourself. “To force it to be read”
total := 0.
largeSet do: [:item | total := total + (item noOfThings)]
Used to connect to an existing database. The argument is string containing the full path of an existing database.
Used to create a new database. The first argument must be a string containing the full path to an existing directory.
Warning: Any database currently residing within this directory will be overwritten.
The second argument is the string that is used to prefix the files for the new database. It should be no longer than 3 characters.
Answers the currently connected database.
Answers true if the target directory already contains a valid database. The first argument must be a string containing the valid path to an existing directory.
exFormat, exLocking, exObjectLoad, exObjectSave, exProxy, exSlice, exTenacity and exTransaction
Answers the corresponding exception.
Answers the format number that will be given to databases created with this version of Tenacity.
Answers a Boolean indicating whether databases will be locked during development time.
Specifies whether databases will be locked during development time.
Answers the version number of Tenacity.
Adds a slice to the database and indicates that this slice will be the default. The first argument is an Integer (in the range 0 to 255) giving a reference number to the slice. The second argument is a string containing the full path to the directory to contain the data. Identifying a default slice is a convenient way to use slices without explicitly identifying them when making an object persistent. Your database will have no memory of which slice is the default, so when reconnecting you should identify a default if one is required.
As #addDefaultSlice:directory: but enables a number of divisions to be specified in the slice. The number of divisions should be a power of 2 (e.g. 128 or 256). Divisions in a slice are used to spread the load. The number of divisions you specify will depend on the estimated maximum number of persistent objects to be stored in the slice. If the number is larger than you need the slice will take marginally more space than is needed. If the number is smaller than you need you may notice a significant reduction in the speed of access to the slice when it contains many objects. If the slice contains more than 300000 persistent objects you should consider spreading the load over more slices. A guide as to the number of divisions you need can be obtained from the equation below, although experimenting with the number to suit your hardware is recommended.
max. no of objects recommended per slice = 512 * divisions needed
(A document will be provided with the release version that makes recommendations for the sizing of divisions with regard to the possible number of objects in the total database.)
If a class name has been changed between releases of your database use this method. The first argument is the name of the class whose objects are to be mutated. The second argument is the class (not the name) the objects will be mutated to.
As #addDefaultSlice:directory: but does not set this slice to be the default.
As #addDefaultSlice:divisions:directory: but does not set this slice to be the default.
Used to discard the current transaction. Use of this method permits the developer to arrange for the discarding of any changes made to persistent objects within a transaction. This should be used only if you are creating transactions without using atomic blocks or wish to clear a transaction during development. The recommended way of discarding changes to objects is to jump out of an atomic block by throwing an exception.
Used to write any current changes to disk before completion of the enclosing atomic block. This should be used only if you are creating transactions without using atomic blocks.
Answers the name of the current default slice.
Sets the default slice. The argument must be the number of a slice that appears in your database.
Answers a string containing the path of the directory that contains the primary files of the database.
Disconnects from a the database. This will unlock single user databases. You should disconnect before reconnecting to another database.
Answers the format number of the database.
Used to commence a garbage collection using the current database. The database will be locked for writing throughout. Although no atomic blocks are needed, Tenacity’s exceptions can still be caught.
As #garbageCollect but also locks the database for reading.
Checks whether a slice with the number given as the argument exists.
Provides a new read transaction.
Note: This method should only be used by those wishing to manage the database without the use of atomic blocks.
newReadWriteTransaction, newReadWriteTransaction, newReadWriteTransaction, newReadWriteTransaction and newReadWriteTransaction
Create new transactions that can be ended with #commitTransaction or #clearTransaction. These method should only be used by developers wishing to manage the database without the use of atomic blocks.
The argument is a block with one argument. This should only be sent during a transaction but has no effect unless sent in a NominatedReadWrite transaction. Each persistent object read during the transaction is put into the block as the argument and those that cause the block to evaluate as true are nominated for writing.
Indicates that the class given as the argument should not be mutated.
Removes the slice from the database. Although it does not actually delete any objects.
Answers the root object of the database.
Sets the root object. If you wish to change to root object in your database you must explicitly set it otherwise changes will not be written to disc.
unlinkInstancesOfClasses:, unlinkInstVars:fromClasses: and unlinkObjects:
Enables certain instances of classes, instance variables or objects not to be written to disk. See “IBM Smalltalk User’s Guide - 126.96.36.199” for more details.
Allows the setting of the number of tries that will be attempted before an ExLocking exception occurs. This is used to tune the database to the performance of the PCs on the network. A slow PC will require less retries than a fast one.
Answers the current setting for the default number of retries.
Answers a persistent version of the object to ensure that it is written to the database. This should only be sent during a transaction that can write objects. If the transaction commits then this object will be written to disk inside the default slice.
As #asPersistent except the slice number is specified.
Answers the persistent proxy to the object.
Sending the message #printString to any persistent object will answer a string representation of the persistent proxy and not the object in the database. To obtain a string representation of the actual object, not its persistent proxy, this method should be used.
asPersistent and asPersistent:
Allows inspection of the object in the database.
Answers true if the argument is a proxy to the same object as self. Otherwise it answers false. Use this in place of #== for comparing persistent objects.
Answers true. Use this to see if an object is persistent.
Answers an array containing the date and time that the object was last written to disk.
Should be sent only during a NominatedReadWrite transaction. Nominates self and all persistent objects referenced by self (directly and indirectly) for writing.
Should be sent only during a NominatedReadWrite transaction. Nominates for writing.
Answers self.The different locks apply only to multiple-user versions of Tenacity. Tenacity intended for single user applications will lock the database from other users throughout the connection. All other references to locks in this manual relate to multiple-user versions only.
Copyright (c) 2002 DirectDual Limited (trading as TotallyObjects).
Totally Objects - DirectDual Limited,
51 Waveney Road,
Suffolk IP1 5DF
Tel: +44 1473 740101
Fax: +44 1473 743567