Copyright © 2001 SMB GmbH
Ozone Documentation License, Version 1
This document is free software; you can redistribute it and/or modify it provided that the terms of the GNU Library General Public License as published by the Free Software Foundation version 2 of the License; and the following terms are met.
The Ozone Database Project <ozone@ozone-db.org>
Included in the ozone distribution is code and documentation made available by other copyright holders and under different licenses. All these licenses allow worldwide, royalty free distribution, whether alone or as part of a larger product. License, copyright and disclaimer of this software is included in this directory.
The document is Copyright (C) 1997-2001 by SMB GmbH, Rohrteichstr. 18, 04347 Leipzig, Germany, All rights reserved.
You must give prominent notice with each copy of the work that the document is used in it and that its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License.
The name ozone must not be used to endorse or promote software products derived from this software without prior written permission of SMB.
Software products derived from this document may not be called ozone nor may ozone appear in their names without prior written permission of SMB.
This document is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details.
Abstract
This document is aimed at developers who would like to contribute to Ozone's codebase or at those who just wish to acquire a better understanding of Ozone's inner workings, either for personal enlightment or to have more information on how to best use Ozone for their own projects. This document concentrates information from Ozone's initiators, contributors and from discussions on the Ozone mailing-lists.
The Ozone Developer's Guide was designed to provide a complete architectural layout of Ozone, as well as insights in its implementation. Together with Ozone's javadoc, the guide will help unleash Ozone's secrets to potential developers.
Table of Contents
Table of Contents
The purpose of this document is to give new developers on Ozone some useful tips and information how to help out and some pointers on the architecture. It is not meant to be a thorough walkthrough of the architecture and design but hopefully to provide you with enough information to make it easy to browse through the code and Java doc and pick it up yourself.
As Ozone is an Open Source project it depends on voluntary effort. Most people involved have regular jobs and contributes to the devlopment of Ozone on their spare time. Because of this and the realtively small number of developers involved, we would like to see that the users of Ozone also becomes developers of Ozone and by empowering them to be able to fix bugs and implement new features they could contribute code to the project and we would have a natural, organic growth of the number of active developers of Ozone
The source code is maintained with the help of CVS. If you do not have CVS or have it but do not know how to use take a look at cvshome Check out /ozone/ozoneDoc at SourceForge using the setup described there. In short: the cvs commands to use for anonymous access is:
~> cvs -d :pserver:anoncvs@cvs.ozone.sourceforge.net:/cvsroot/ozone co ozoneFor developer rw access the settings and commands are as follows:
~> export CVS_RSH=ssh ~> export CVSROOT=:ext:yourSourceforgeId@cvs.ozone.sourceforge.net:/cvsroot/ozone ~> cvs co ozoneAll components are found under the ozone directory. The core is under the server directory. Other things are in either modules or third party. OzoneDoc is special since it is only documentation.
You are now ready to build the core:
~/ozone> cd server ~/ozone/server> build.shor the equivalent build.bat file on Windows
To start ozone set your OZONE_HOME to ~/ozone/server/build and run ozone using the scripts in ~/ozone/server/build/bin. Don't try to run the server by using the scripts in server/bin as the paths will not be set up properly.
To build a binary distribution of the server use the dist.bin target. This will create a ozone-[version]-dev directory that should be set as the OZONE_HOME in order to use it. To build a binary distribution of the server and all modules use the dist.bin target in the ~/ozone root directory.
OZONE_HOME refers to the base location for the compiled, runnable structure, not the CVS project root dir. For version 1.0x the two locations happened to be the same but this is no longer the case for 1.1x.
As mentioned above, there are two possible settings for OZONE_HOME for developers:
Set it to ozone/server/build. This allows for simple recompilation to get updated classes which is fast and simple.
Set it to ozone/server/build/ozone-1.1.X-dev This is the server distribution dir i.e. what the structure will look like in the binary distributions. It is smaller since all classes are jared but it takes longer to build (since the dist.bin target needs to be invoked to produce it). I typically copy the ozone-1.1.X-dev directory to somewhere such as /test/ozone-1.1.X-dev and point OZONE_HOME there so that i can do more formal testing of my system.
It might be easier to start with something small before attempting to take on a major feature enhancement. Things like javadoc on members and public attributes, implementations of methods that are emty etc. are always welcome but more probably you have found a bug in ozone when working on your application and you are reading this document to try figure out where to start looking for possible ways to fix it. If non of this is the case and you the CHANGES file contains a TODO list of things needed.
Use Sun's "Code Conventions for the Java Programming Language" (4 spaces instead of tabs, comment style etc.).
Please use english in your comments.
Once you have something working send a diff to the ozone-dev list with an explanation of your original problem and why you think this way of fixing it is good. A diff is created by running
cvs diff -u [file]
If the design of what you are trying to to do is not yet clear (which is true of most new features), then it is likely to benefit from more work and community input. In this case, make a list of issues, or write documentation including rationales for how one would use the feature. Discuss it with the others at ozone-dev and see what they think. The intention is arrive at some kind of rough community consensus before changing the "official" CVS.
If you continue to submit patches you will probably be given a rw CVS access so that you can commit changes directly. The process of getting commit access is as follows:
Old committers can propose new committers
Old committers can/should give their votes when a new committer is proposed
The vote can be -1, +0 or +1
A -1 needs a explanatory statement
A new committer needs at least 3 x +1 and no -1 to be accepted
3 months of inactivity automatically removes commit right from a developer
We need to expand our Junit tests so if it is possible to write test for a fix or feature add-on you're working on that would be great.
Here's a quick overview of our approach to Junit tests:
For each concerete class org/ozoneDB/Foo.java, a UnitTest named test/ozoneDB/FooTest.java is created.
For each package, an AllTestSuite.java is created to include all Unit Test in the package.
Unit test should be as comprehensive as possible and should cover methods that are more than get/set.
All TestSuite's must be passed before codes are committed into the CVS.
The package org.ozoneDB.test provide the JUnit test framework for Ozone. This version is compatible with JUnit 3.6.
OzoneTestCase is the base for all Ozone TestCase. It includes access to the database setup by the TestRunner.
The following is an example for to write a OzoneTestCase.
packge org.ozone; import junit.framework.*; import org.ozoneDB.test.OzoneTestCase; import org.ozoneDB.test.simple.*; package FooTestCase extends OzoneTestCase { public static Test suite() { TestSuite suite = new TestSuite(); suite.addTestSuite(FooTestCase.class); return suite; } public void testCreate() { // db() provide the access to database setup by TestRunner Auto auto = (Auto)db().createObject(AutoImpl.class.getName()); assertNotNull(auto); // Test away!!! } }
Once you have the TestCase written, you will want to run the Test. OzoneTestRunner provides the environment to run Unit Test for Ozone.
Usage: ojvm org.ozoneDB.test.OzoneTestRunner {-local | -remote [-host=HOST] [-port=PORT]} [-debug] -local: use local database -remote: use remote database -host=HOST: the database server host -port=PORT: the database server port -debug: enable the debugging output
Note: The OzoneTestRunner's logging is controled by Log4J. You can use the log4j in your TestCase.
For further reference see Log4J at apache and the JUnit homepage.
Table of Contents
The Ozone architecture, very generally represented by the diagram further below, has four main layers:
Client
This is the client application area; the client obtains a connection to an Ozone server, connection that can be shared by many threads. The client application interacts with the database API to create, delete, update and search persistent objects in the underlying Ozone storage
Network
The network layer is where the Ozone protocol plays a role similar to RMI. It carries method invocation information targeted at persistent objects, in addition to all other commands relayed to the Ozone server.
Server
The server manages client connections, security, transactions, and incoming method invocations from the clients. If required, it is in charge of invoking methods on persistent objects, therefore tightly interacting with the underlying object storage facility. The server maintains a transactionally safe environment for multiple clients that access persistent objects through a remote proxy.
Storage
The storage system is always accessed through an Ozone server. The storage is responsible for object persistence, clustering, object identification, and other task pertaining to low-level database-like operations.
High-level view of Ozone's architecture
The above architecture illustrate the most obvious use of Ozone, which acts as a remote server; but a client application can instantiate an Ozone server as part of its VM, therefore removing the network layer. Such a scheme would be convenient for standalone use. In such a case though, this behavior is implemented through a proxy pattern, which makes client code portable to both configurations (local and remote). We will see further below what this proxy scheme consists of.
Database objects are the persistent objects designed by developers to fullfill their application logic needs. Database objects implement a given interface (in more concrete terms, a Java interface that extends org.ozoneDB.OzoneRemote), and this interface is the "visible" side of database objects. There is only one instance of a database object, which lives inside the database server. This database object is controlled via proxy objects.
A given proxy object represents its corresponding database object - inside the client applications and inside other database objects. A proxy object can be seen as a persistent reference. Proxy classes are automatically generated out of the database classes by the Ozone post-processor and implement the same public interface as their respective database object counterpart - which means that they also implement the OzoneRemote interface that their corresponding database object implements.
All ozone API methods return proxies for the actual database object inside the database. Therefore, the client deals with proxies only. However, this is transparent to the client: proxies can be used as if they were the actual database objects, since they implement the same interface.
Database objects are different from ordinary Java objects (other systems and specs, like JDO, respectively call them "primary" and "secondary", or "first-class" and "second-class"). Only one instance of a given database object reference exists in the database, as opposed to standard Java objects, which are treated in a "by-copy" fashion each time they are serialized. By analogy, database objects are a bit like rows in a relational database table, and members of these database objects that are standard Java objects correspond to the columns in the row - database object members would correspond to links to other tables, if we push the analogy.
Standard Java objects that are part of a database object can be directly used from the client application and from other database objects. In fact, database objects are sets of ordinary objects. For example the class "Car" contains several String members which are objects. One object of type Car and the dependent String members is a database object. In this regard modeling an ozone database is comparable to modeling a relational database.
Database objects in the server are loaded into memory as a whole when someone invokes a method on the object. Since the client actually uses a proxy to the server object, it gets loaded when a method on the proxy is invoked. This is a very important feature as it ensures that objects in the server are only loaded when they are needed. As an example, if the proxy you are working with contains a collection, the collection and the proxies in the collection will be available on the client but none of the corresponding objects in the server will be loaded. The object server loads objects based on client use of the proxy objects which helps reduce the load and memory usage in the server by only loading objects when needed.
Table of Contents
This abstract class implements the OzoneInterface interface and, as such, the behavior common to most instances of OzoneInterface. This class acts as a proxy [pattern: Proxy, source: ...] hiding the type of access to Ozone (remote or local) behind a single abstraction. Client applications need only "opening" an underlying physical database using the proper URL syntax, which this class interprets to return the proper child class instance of ExternalDatabase, namely: org.ozoneDB.LocalDatabase or org.ozoneDB.RemoteDatabase.
Concretely, client applications obtain an ExternalDatabase intance through one of the static openDatabase(...) methods. The factory methods [pattern: Abstract Factory Method, source: ...] expect the URL string passed to them to begin with one of the following patterns: "ozonedb:local" or "ozonedb:remote", corresponding respectively to LocalDatabase and RemoteDatabase.
Subclasses of ExternalDatabase are expected to implement the following abstract method [pattern: Template Method, source: ...]:
protected abstract DbClient newConnection() throws Exception;
An instance of LocalDatabase is used when application and Ozone server are in the same Virtual Machine, meaning that there is no network layer separating application and server. The LocalDatabase's newConnection() method is implemented in the following way:
protected DbClient newConnection() throws Exception { return new DbLocalClient( this, theEnv, userName ); }
An instance of RemoteDatabase is used, as opposed to a LocalDatabase, when the client application is communicating with a remote Ozone server. As such, a RemoteDatabase instance relays commands to a remote server; the following illustrates the type of connection used in such a case:
protected DbClient newConnection() throws Exception { if (portNum == 0) { throw new DbNotOpenExc(); } DbClient connection = new DbRemoteClient( this, hostname, portNum, userName ); sendCommand( new DbOpen( userName ), true, connection ); return connection; } }
The org.ozoneDB.core.DBRemote.DbLocalClient class is really a "dummy" connection: it does not internally contain a network link. The class' role is to provide the illusion of a network so that ExternalDatabase can be used in a local context. The following class diagram illustrates the DbClient class and its children.
org.ozoneDB.core.DBRemote.DbClient class hierarchy
One instance of ExternalDatabase can be used safely in a multi-threaded context, although the ExternalDatabase class does not maintain one connection per thread, which is considered too resource-intensive. Instead, ExternalDatabase maintains a pool of connections (i.e.: org.ozoneDB.core.DbRemote.DbClient instances) [pattern: Pool, source: ...] that are recycled among many client threads, except in the context of a transaction: in such a case, the thread is registered with a connection until the transaction is over, after which the connection is pooled.
The following excerpt illustrates that logic:
//code from sendCommand( DbCommand command, boolean waitForResult ) Thread thread = Thread.currentThread(); AbstractTransaction txOfThread = (AbstractTransaction)txTable.elementForKey( thread ); if (txOfThread != null) { DbClient connection = txOfThread.connection; return sendCommand( command, waitForResult, connection ); } else { DbClient connection = null; try { connection = acquirePooledConnection(); return sendCommand( command, waitForResult, connection ); } finally { releasePooledConnection( connection ); } }
This code shows that if a thread is associated to a transaction, the connection encapsulated within that transaction object is used.
The ExternalDatabase class implements the org.ozoneDB.OzoneInterface which, as shown below, defines the behavior expected from classes that allow interaction with the Ozone server - and the latter's subcomponents:
public OzoneProxy createObject( String className ) throws Exception; public OzoneProxy createObject( String className, int access ) throws Exception; public OzoneProxy createObject( String className, int access, String objName ) throws Exception; public OzoneProxy createObject( String className, int access, String objName, String sig, Object[] args ) throws Exception; public OzoneProxy copyObject( OzoneRemote rObj ) throws Exception; public void deleteObject( OzoneRemote rObj ) throws Exception; public void nameObject( OzoneRemote rObj, String name ) throws Exception; public OzoneProxy objectForName( String name ) throws Exception; public OzoneProxy[] objectsOfClass( String name ) throws Exception; public Object invoke( OzoneProxy rObj, String methodName, String sig, Object[] args, int lockLevel ) throws Exception; public Object invoke( OzoneProxy rObj, int methodIndex, Object[] args, int lockLevel ) throws Exception; public OzoneCompatible fetch( OzoneProxy rObj, int lockLevel ) throws Exception; public void reloadClasses() throws Exception; public Node xmlForObject( OzoneRemote rObj, Document domFactory ) throws Exception; public void xmlForObject( OzoneRemote rObj, ContentHandler ch ) throws Exception; public OzoneService service (int serviceHandle) throws Exception;
As can be seen above, OzoneInterface defines methods pertaining to the management of database objects - through their corresponding proxies (instances of OzoneProxy); these methods correspond to the create-read-delete operations - the update operations being performed through the methods of the proxy - which are shared by its corresponding database object.
Proxies retrieved by client applications delegate their method's behavior to their corresponding database object. This delegation is not direct, but an illusion: indeed, a proxy always goes through an OzoneInterface instance to "reach" its corresponding database object. For example, when a proxy is retrieved from an ExternalDatabase instance, it keeps a link to the latter, and delegates its method calls to it. The method call is relayed to the Ozone server using Ozone's custom protocol. The Ozone server then performs the method call on the database object instance that corresponds to the proxy from which the call originates, in a transactionnaly safe manner, and returns the result, if any.
The following code excerpt was generated by the Ozone Post-Processor (OPP). It belongs to the Car example delivered with Ozone (and featured on the web site), and is in fact the Car proxy. The delegation mechanism is clearly visible:
import org.ozoneDB.*; import org.ozoneDB.core.ObjectID; import org.ozoneDB.core.Lock; import org.ozoneDB.core.ResultConverter; /** * This class was automatically generated by ozone's OPP. * Do not instantiate or use this class directly. */ public final class CarImpl_Proxy extends OzoneProxy implements newtradetest.ozone.Car { static final long serialVersionUID = 1L; public CarImpl_Proxy() { super(); } public CarImpl_Proxy (ObjectID oid, OzoneInterface link) { super (oid, link); } public java.lang.String name () { try { Object target = link.fetch (this, Lock.LEVEL_READ); if (target != null) { return (java.lang.String)ResultConverter.substituteOzoneCompatibles (((CarImpl)target).name()); } else { Object[] args = {}; Object result = link.invoke (this, 8, args, Lock.LEVEL_READ); return (java.lang.String)result; } } catch (RuntimeException e) { e.fillInStackTrace(); throw e; } catch (Exception e) { e.fillInStackTrace(); throw new UnexpectedException (e.toString()); } } public void setName (java.lang.String arg0) { try { Object target = link.fetch (this, Lock.LEVEL_WRITE); if (target != null) { arg0 = (java.lang.String)ResultConverter.substituteOzoneCompatibles (arg0); ((newtradetest.ozone.CarImpl)target).setName(arg0); } else { Object[] args = {arg0}; Object result = link.invoke (this, 15, args, Lock.LEVEL_WRITE); } } catch (RuntimeException e) { e.fillInStackTrace(); throw e; } catch (Exception e) { e.fillInStackTrace(); throw new UnexpectedException (e.toString()); } } } }
When "outside" of the Ozone server, a proxy is "linked" to an ExternalDatabase instance, but it does not "know" about it. In fact, it has to go through a hack to deduct whether it is remote or local mode: when a given method is called on it, the proxy calls its link's (OzoneInterface instance) fetch() method; if the call returns null, it knows it is in remote mode (since no corresponding database object was returned), and thus goes through a remote method invocation through its link's invoke() method. The following code illustrates this:
Object target = link.fetch (this, Lock.LEVEL_WRITE); if (target != null) { arg0 = (java.lang.String)ResultConverter.substituteOzoneCompatibles (arg0); ((newtradetest.ozone.CarImpl)target).setName(arg0); } else { Object[] args = {arg0}; Object result = link.invoke (this, 15, args, Lock.LEVEL_WRITE); }
When inside the server, a proxy calls its link's fetch() method, which returns its corresponding database object; it then directly delegates the method call to the database object, without further ado. This approach is way faster since there is no remote invocation.
In the case of "in-server" proxies, the OzoneInterface link is an instance of org.ozoneDB.Database, which is the database abstraction used within an Ozone server.
An automatic memory management structure (i.e. Garbage Collection) would simplify development of systems using complex datastructures (such as circular referenced structures). This is in line with the Java style of memory management and as Ozone is works as a persistent Java heap it makes sence to not force users to do manual memory management for thier persistant objects when they do not have to do it for other objects.
A GarbageCollector-enabled system may run faster, because at peak uses, object deletions may be delayed until the GarbageCollector is run.
Development times will be faster because users do not have to do memory management manually.
The persistant garbage collector is a mark-sweep-garbage collector which acts transparently in the background. Basically, it works as follows:
There are three disjoint virtual sets of objects (possiblyReachable, surelyReachable and doneReachable). All objects initially belong to possiblyReachable. The root objects are moved to surelyReachable. Then, every surelyReachable object is processed for references it has, and these references are added to the surelyReachable set. The processed objects is moved to the doneReachable set.
The process has ended if the surelyReachable set is empty. The objects in the doneReachable set are reachable and the objects remaining in the possiblyReachable set are not. These objects are then reclaimed.
The process is done at object granularity so that there is no need to stop database operations for running the garbage collector.
The GarbageCollector considers following database objects as reachable (which will not be garbage collected):
Named objects
Object whose proxy was sent from the server to the client during the current session and this session was not already closed
Objects which are referenced by clients during a database session. I.e. It is an object which is on the stack of a current transaction thread
It is an object which is reachable by a reachable object
Because it sends a notification message to the server on the finalize() call
Because the connection to the client is lost.
Other objects are considered as "unreachable" and will be reclaimed once a GarbageCollector run has finished.
Not only are objects reachable which have a name, also objects are reachable which are reachable by reachable objects. These "indirectly reachable" objects do not have names and still may not be reclaimed.
The GarbageCollector runs at object granularity, thus it does not block the rest of the database users. (It still disturbs because it should cause heavy IO-traffic).
Referential integrity is maintained. Since unreachable OzoneObjects are unreachable, they no longer need to support referential integrity. The other objects which are reachable do not loose their referential integrity property by being reachable (reachable objects are not touched).
Objects, which are reclaimed, will be called via their "onDelete()" method. Thus, even the unreachable object set maintains referential integrity.
The trick to not reclaim fresh objects which are only held by the current transaction is:
At garbageCollect start, first let all transaction finish which currently run before walking the object graph. For new transactions, new objects are marked as rechable at their creation time.
We wait until all objects which are held by current transactions on their call stack will go reachable by other means.
Besides of introducing a mark-sweep garbage collector for persistent objects, it changes a bunch of other things which were required to make the garbage collector work. Among these are:
Object pinning: The internal API and implementation has been changed to allow objects to be pinned in RAM. It is implemented independently of locking.
client proxy tracking: All proxy objets which are sent to clients are tracked (to know what objects are referenced from clients). This produces additional network traffic (every time a proxy object dies, it notifies the server about it).
inter-ObjectContainer-call callstack tracking: This tracking enables to know "who calls whom" in order to track calls between the different garbage collection sets (which is required to track references "walking" between these sets)
partly changed Exception handling in internal API: Nearly Every method threw the exception Exception. This made the method user (which may be the GarbageCollector) unable to differentiate between the different reasons an Exception is thrown. Thus, exceptions are declared more fine grained.
The only issue which seems to remain to impact the theoretical implementability of such a garbage-collector is the way transaction support is implemented. The reason is, that transactions, which are eligible to rollback, may recreate objects on rollback which may be handled as dead by the garbage-collector if the structure of the implementation of transaction support is not carefully observed.
How does it take into account that proxies (object references) can not only exist in other objects but also on the client side?
I'm tracking OzoneProxys which leave the database VM to database clients. On garbage collection (finalize()), they "sign off" themselves (send the ozoneDB server that now the reference they represented is no longer existent. If the connection to the client breaks, all registered OzoneProxy references are declared dead. This is inefficient (due to higher network load), and currently it is not 100% secure (Because ResultConverter.substituteOzoneCompatibles() does not substitute recursively as it should do), but there is a solution for "embedded Proxys" which is complicated but implementable. Before implementing the complicated solution, I'd like to know from the community whether an OzoneObject exists which could return an "embedded proxy" to its caller (e.g. a proxy reachable within the returned object but where the returned object is not a proxy object itself). If the complicated solution is not needed, it does not have to be implemented.
Referential Integrity: One issue we might need to investigate is the case where calling the onDelete() method actually makes that object reachable again: Is this handled by the garbage collector properly? This is a fine example for a sophisticated garbage collector test case.
GarbageCollector.processSurelyReachableObjectsWhichHaveToBeMarkedAsSuch() What to do in the catch (TransactionExc e) block?
GarbageCollector.interceptInvocationPre() There is a design decision which heavily affects performance: - Do we first check for the possibility of an OzoneProxy as parameter or - Do we first check for the cross of the border of the different sets of database objects?
GarbageCollector.GarbageCollectorProxyObjectIdentificationObjectOutputStreamMaybe one GarbageCollectorProxyObjectIdentificationObjectOutputStream per GarbageCollector is sufficient, but there is state maintained in the ObjectOutputStream and flushing state during time another thread is using the ObjectOutputStream as well produces situationes which are unexpected by the developers of ObjectOutputStream. So this is currently not considered.
Table of Contents
Once a branch is created no new functionality should go in there, only bug fixes. All new functionality goes into HEAD which is always regarded as in a state of flux.
The basic idea is to have a structure as follows [v][version_numbers][-type][-state] where type is branch, release etc. and state is alpha, beta etc.
* When version numbers are included in the name, change dots to underscore e.g. "1.1.x" becomes "v1_1_x"
* When branching always create a tag with the type suffix -root first e.g. "v1_1_x-root"
* Branches should have the type suffix -branch e.g. "v1_1_x-branch"
* Releases are tagged with the type suffix -release e.g. "v1_1-release"
The "x" in the end of the version number marks the scope of the branch where as the additional "dev" means it is from HEAD. I.e. after the branch for "1.1.x" is created HEAD's build.properties is updated from "1.1.x-dev" to "1.2.x-dev".
"x" and "x-dev" should never appear in normal tags but only when creating the branch.
We create one branch for each release point (1.1, 1.2 etc) and subsequent releases within this branch (1.1.1, 1.1.2 etc) are tagged. e.g. working on "v1_1_x-branch" and releasing the first alpha, the tag would be "v1_1-release-alpha". Before tagging the release , build.properties is updated to match the tag name so that the tag "v1_1-release-alpha" would be released as "ozone-1.1-alpha" and have "version = 1.1-alpha" in build.properties:
The following state suffixes are used:
-alpha This means the first cut after a branch. All functionality should be there but proper testing has not been made other than it builds and "seems to run fine".
-beta When all tests run without problems and sufficient time has passed after the alpha a beta is released.
If bugs are encountered after a beta release there might be need to release a second beta if so just add an incrementing number to the -beta suffix e.g. -beta2 ( note: -beta is considered -beta1 so the number 1 is never used.)
The final version would just not have a prefix e.g. "v1_1-release"
If bugs are found in 1.1 then point releases fix them e.g. "v1_1_1-release" fixes bugs in 1.1 (v1_1-release) "v1_1_2-release" fixes bugs in 1.1.1 (v1_1_1-release) etc.
If necessary (major changes was made) it is possible to go through a beta cycle before releasing the bug fixed version e.g. "v1_1_1-release-beta" is first released as "ozone-1.1.1-beta" and when stable "v1_1_1-release" goes out as "ozone-1.1.1"
It is also possible, if really needed, to create a second branch off the current one: e.g. tag "v1_1_1_x-root" branch "v1_1_1_x-branch"
Another reason for creating a branch would be to explore some very different functionality that is not decided if it should go into HEAD or not. In this case use the state suffix -devel when creating the branch and append some kind of descriptive name after devel separated with a dash. e.g. "v1_1-devel-garbage_collection" or "v1_1-devel-client_call_backs" would be good names.