Object level optimistic locking

Optimistic locking provides the following semantics for handling concurrent data access: locks on the same object can be acquired by clients in parallel. However, only the first client trying to commit changes on the object succeeds; all other clients receive an exception when they commit.

It is important to understand that read only access is also protected by optimistic locks: a client that locks and only reads an object instance even receives a lock violation upon committing the context if another client has modified the object in the meantime.

Looking at this semantics from another perspective, this means that the state of the object is locked: many processes can access the same object in parallel, as long as the state is not modified. As soon as the first process modifies the state and commits changes, all other processes trying to commit (even without changes to the state) will fail.

This means that lock violation exceptions may occur at the end of the transaction, when the client tries to commit, when using optimistic locking semantics.

The advantage of optimistic locking is that it allows concurrent access to data for as long as possible and that locking consumes little resources (compared to pessimistic locking). The disadvantage of optimistic locking is that the client is informed late about failing transactions (just before the client wants to commit), which can mean that work performed by the user is lost.

Optimistic locking is a recommended strategy when clients often need to access data in parallel and/or chances of concurrency violations are low in case of parallel access. Another reason for using optimistic locking is when the loss of work through a concurrency violation is acceptable or a compensating merge operation can be implemented easily. These conditions often apply to web applications.

To use object-level optimistic locking, you need to:

·    specify a locking strategy in the mapping on a type

·    set up a PersistentOptimisticLockServer in the DataDomain

·    configure optimistic locking semantics for the Context

Specifying optimistic locking strategies on a type

To use optimistic locking, you also need to specify a locking strategy to be used. Genome supports different implementation strategies for optimistic locking, which are currently:

·    optimistic locking using a row-version field in the database

·    optimistic locking using the object state

The strategy to be used is specified in the mapping file on the corresponding type.

Optimistic locking using a row-version field

With this strategy, Genome uses a designated version field on the class to detect changes of the object state. You can configure it on a type by using the <OptimisticLock/> element:

<Type name="Customer">

       <OptimisticLock fieldName="Version"/>

</Type>

By default, the version field is called __optLockVersion. That name can be overridden by using the fieldName attribute. Please note that the version field is not available as a property on the domain model.

Depending on the underlying database platform, the row-version field is mapped to the field type which is either automatically updated by the database (Microsoft SQL Server) or else Genome explicitly updates the field with a new version when updating rows (Oracle, IBM DB2 and Microsoft SQL CE).

Optimistic locking using the object state

With this strategy, Genome uses the full state of the object to detect changes. In other words, Genome compares all fields of the loaded state with the current state in the database to determine whether changes occurred. You can configure it on a type by using the <StateOptimisticLock/> element:

<Type name="Customer">

       <StateOptimisticLock view="MyLockView"/>

 

       <View name="MyLockView">

              <Member>CustomerId</Member>

              <Member>CompanyName</Member>

       </View>

</Type>

By default, Genome uses the full object state (see view full) to build the lock version. However, the view attribute can be specified to use another view, which can be configured by using Genome’s partial loading feature. This can be useful for excluding large object state, like binary images, from the lock version.

Choosing between row-version and object state optimistic locking strategy

The state optimistic locking has the least impact on the database model, as it does not require any additional database field. This can be useful for existing databases, especially when other clients exist that cannot update a newly introduced version field that would be required by row-version optimistic locking.

Row-version optimistic locking, on the other hand, is more efficient in terms of resource consumption, as the lock state requires less memory on the client and update queries only require comparing a single row-version field instead of all the state fields. Therefore, this strategy is recommended when you can influence the database schema and all the clients accessing the database.

Setting up a PersistentLockServer in the DataDomain

Another prerequisite for using object-level optimistic locking is setting up a PersistentLockServer in the DataDomain. Please note that the lock server can only be set up when you instantiate the DataDomain. You cannot add a lock server to an existing DataDomain instance:

DataDomainConfiguration ddConfig = new DataDomainConfiguration();

ddConfig.Schema = DataDomainSchema.LoadFrom(SCHEMA_ASSEMBLY);

ddConfig.ConnectionString = "Server=(local);Database=Northwind;Integrated Security=True;User ID=;Pwd=";

ddConfig.Role = DATADOMAIN_ROLE;

 

ddConfig.LockServers.Add(typeof(object), PersistentOptimisticLockServer.Value);

 

DataDomain newDataDomain = new DataDomain(ddConfig);

The lock server is set for a given type (in this case System.Object) and automatically applies to all derived types as well. Please refer to the User’s Guide to learn more about how to initialise and use the DataDomain in your project.

Configuring and using optimistic locking semantics on the context

After specifying a locking strategy and initialising a DataDomain with a PersistentLockServer, you can use a context with optimistic locking semantics to perform object level locking.

The following code extends the previous sample to use optimistic locking semantics. Note that desired locking semantics are passed to the context factory as a parameter. Locking semantic bindings for a type automatically apply to all derived types, unless a more specific binding is specified:

// context locking semantics setup

LockingSemanticsBindings lockingSemantics = new LockingSemanticsBindings();

lockingSemantics.Add(typeof(object), OptimisticLockingSemantics.Value);

 

// create two independent contexts

Context ctx1 = LocalContext.Create(lockingSemantics);

Context ctx2 = LocalContext.Create(lockingSemantics);

 

// lookup customer from the database in ctx1

Customer customerRef1 = (from c in Helper.DB.Extent<Customer>(ctx1)

                  where c.CustomerId == "ALFKI"

                  select c).FirstOrDefault();

 

 

// refer to the same customer in ctx2

Customer customerRef2 = ctx2.Pin(customerRef1);

 

// lock customer in both contexts

ctx1.Lock(customerRef1);

ctx2.Lock(customerRef2);

 

// update customer in both contexts

customerRef1.CompanyName = "cref1";

customerRef2.CompanyName = "cref2";

 

// commit both contexts

ctx1.Commit();

ctx2.Commit();

The sample above would throw a TechTalk.Genome.LockViolationException ("(#GEN0500) Optimistic lock violation: the object "…" of type "…" could not be committed, because its server version has changed or there is a pessimistic lock on the object.") when committing the second context (ctx2.Commit()).

Note that lock violations already occur if the locked object is only read (no state modification) when another client modifies the object state in the meantime. Hence, the following code would still throw a LockViolationException, even though customer is not modified in ctx2:

// lock customer in both contexts

ctx1.Lock(customerRef1);

ctx2.Lock(customerRef2);

 

// update customer in the first contexts

customerRef1.CompanyName = "cref1";

 

// commit both contexts (ctx1 first) -> causes lock violation on ctx2.Commit()

ctx1.Commit();

ctx2.Commit();

However, if you reverse the order and commit the read only context first, no lock violation occurs, as the customer’s state has not yet been modified when ctx2 is committed:

// lock customer in both contexts

ctx1.Lock(customerRef1);

ctx2.Lock(customerRef2);

 

// update customer in the first contexts

customerRef1.CompanyName = "cref1";

 

// commit both contexts (ctx2 first) -> will work

ctx2.Commit();

ctx1.Commit();

Accessing and setting the lock version of an object

When disconnecting and re-enrolling object-proxies from/to a context, you can extract and restore the acquired lock version of the object.

To retrieve the lock version of a previously locked object, OptimisticLockSemantics.GetClientVersion(object, Context) can be used:

IOptimisticLockVersion version =

OptimisticLocking.GetClientVersion(customerRef1, ctx1);

To enroll a saved lock version to an object, use the corresponding Context.Lock() overload, passing the version as an instance of OptimisticLockArgs():

ctx1.Lock(customerRef1, new OptimisticLockArgs(version));

Note that the object version returned by OptimisticLocking.GetClientVersion() is not directly serializable. To serialize and deserialize, you need to call the following static methods:

·    OptimisticLockSemantics.VersionToByteArray(IOptimisticLockVersion)

·    OptimisticLockSemantics.VersionFromByteArray(object, byte[])

To deserialize an object version from a byte array, you need to pass the persistent object that will be locked with the deserialized version, since the deserialization method may depend on the object’s locking configuration.

The following example shows how to save an object proxy with its version to an ASP.NET page’s view state:

// retrieve the instance of an Employee

Employee e = …

 

// lock in this context

Context.Current.Lock(e);

 

// store proxy and version in view state

ViewState["obj"] = e;

ViewState["objversion"] = OptimisticLockSemantics.

                          VersionToByteArray(

                            OptimisticLockSemantics.GetClientVersion(e));

And the following code enrolls the proxy from the view state in the current context with the saved lock version:

// restore proxy from view state

Employee e = (Employee)ViewState["obj"];

 

// retrieve serialized version

byte[] objVersion = (byte[])ViewState["objversion"];

 

// lock object with deserialized version in current context

Context.Current.Lock(e, new

                          OptimisticLockArgs(

                            OptimisticLockSemantics

                            .VersionFromByteArray(e, objVersion));

This infrastructure is also used by WOP, where serialized DTOs store the object identity and the optimistic lock version. WOP automatically connects the object back with the stored version when deserialization occurs.



Overview about object level locking Object level pessimistic locking