In optimistic locking system no locks are used to prevent collision: any user can read an object into the memory and work on it at any time. However, before the client can save its modifications back to the database, a check should take place verifying that the item did not change since the time of initial read (no collision occurred). If a collision is detected it should be resolved according to your application logic. Typical solutions are:
Let's look at an example realization.
We will use a db4o database containing objects of Pilot class and a separate thread to create a client connection to the database, retrieve and modify objects.
01public void Run() 02
{ 03
try { 04
IObjectSet result = _db.Get(typeof(Pilot)); 05
while (result.HasNext()){ 06
Pilot pilot = (Pilot)result.Next(); 07
long objVersion = _db.Ext().GetObjectInfo(pilot).GetVersion(); 08
09
/* save object version into _idVersions collection 10
* This will be needed to make sure that the version 11
* originally retrieved is the same in the database 12
* at the time of modification 13
*/ 14
long id = _db.Ext().GetID(pilot); 15
_idVersions.Add(id, objVersion); 16
17
Console.WriteLine(Name + "Updating pilot: " + pilot+ " version: "+objVersion); 18
pilot.AddPoints(1); 19
_updateSuccess = false; 20
RandomWait(); 21
if (!_db.Ext().SetSemaphore("LOCK_"+_db.Ext().GetID(pilot), 3000)){ 22
Console.WriteLine("Error. The object is locked"); 23
continue; 24
} 25
_db.Set(pilot); 26
/* The changes should be committed to be 27
* visible to the other clients 28
*/ 29
_db.Commit(); 30
_db.Ext().ReleaseSemaphore("LOCK_"+_db.Ext().GetID(pilot)); 31
if (_updateSuccess){ 32
Console.WriteLine(Name + "Updated pilot: " + pilot); 33
} 34
Console.WriteLine(); 35
/* The object version is not valid after commit 36
* - should be removed 37
*/ 38
_idVersions.Remove(id); 39
} 40
41
} finally { 42
_db.Close(); 43
} 44
}
01Public Sub Run() 02
Try 03
Dim result As IObjectSet = _db.Get(GetType(Pilot)) 04
While result.HasNext 05
Dim pilot As Pilot = CType(result.Next, Pilot) 06
Dim objVersion As Long = _db.Ext.GetObjectInfo(pilot).GetVersion 07
' save object version into _idVersions collection 08
' This will be needed to make sure that the version 09
' originally retrieved is the same in the database 10
' at the time of modification 11
Dim id As Long = _db.Ext.GetID(pilot) 12
_idVersions.Add(id, objVersion) 13
Console.WriteLine(Name + "Updating pilot: " + pilot.ToString() + " version: " + objVersion.ToString()) 14
pilot.AddPoints(1) 15
_updateSuccess = False 16
RandomWait() 17
If Not _db.Ext.SetSemaphore("LOCK_" + _db.Ext.GetID(pilot).ToString(), 3000) Then 18
Console.WriteLine("Error. The object is locked") 19
' continue 20
End If 21
_db.Set(pilot) 22
' The changes should be committed to be 23
' visible to the other clients 24
_db.Commit() 25
_db.Ext.ReleaseSemaphore("LOCK_" + _db.Ext.GetID(pilot).ToString()) 26
If _updateSuccess Then 27
Console.WriteLine(Name + "Updated pilot: " + pilot.ToString()) 28
End If 29
Console.WriteLine() 30
' The object version is not valid after commit 31
' - should be removed 32
_idVersions.Remove(id) 33
End While 34
Finally 35
_db.Close() 36
End Try 37
End Sub
A semaphore is used for locking the object before saving and the lock is released after commit when the changes become visible to the other clients. The semaphore is assigned a name based on object ID to make sure that only the modified object will be locked and the other clients can work with the other objects of the same class simultaneously.
Locking the object for the update only ensures that no changes will be made to the object from the other clients during update. However the object might be already changed since the time when the current thread retrieved it. In order to check this we will need to implement an event handler for the updating event:
1public void RegisterCallbacks() 2
{ 3
IEventRegistry registry = EventRegistryFactory.ForObjectContainer(_db); 4
// register an event handler to check collisions on update 5
registry.Updating += new CancellableObjectEventHandler(OnUpdating); 6
}
01private void OnUpdating(object sender, CancellableObjectEventArgs args) 02
{ 03
Object obj = args.Object; 04
// retrieve the object version from the database 05
long currentVersion = _db.Ext().GetObjectInfo(obj).GetVersion(); 06
long id = _db.Ext().GetID(obj); 07
// get the version saved at the object retrieval 08
IEnumerator i = _idVersions.GetEnumerator(); 09
10
long initialVersion = (long)_idVersions[id]; 11
if (initialVersion != currentVersion) 12
{ 13
Console.WriteLine(Name + "Collision: "); 14
Console.WriteLine(Name + "Stored object: version: " + currentVersion); 15
Console.WriteLine(Name + "New object: " + obj + " version: " + initialVersion); 16
args.Cancel(); 17
} 18
else 19
{ 20
_updateSuccess = true; 21
} 22
}
1Public Sub RegisterCallbacks() 2
Dim registry As IEventRegistry = EventRegistryFactory.ForObjectContainer(_db) 3
' register an event handler to check collisions on update 4
AddHandler registry.Updating, AddressOf OnUpdating 5
End Sub
01Private Sub OnUpdating(ByVal sender As Object, ByVal args As CancellableObjectEventArgs) 02
Dim obj As Object = args.Object 03
' retrieve the object version from the database 04
Dim currentVersion As Long = _db.Ext.GetObjectInfo(obj).GetVersion 05
Dim id As Long = _db.Ext.GetID(obj) 06
' get the version saved at the object retrieval 07
Dim i As IEnumerator = _idVersions.GetEnumerator 08
Dim initialVersion As Long = CType(_idVersions(id), Long) 09
If Not (initialVersion = currentVersion) Then 10
Console.WriteLine(Name + "Collision: ") 11
Console.WriteLine(Name + "Stored object: version: " + currentVersion.ToString()) 12
Console.WriteLine(Name + "New object: " + obj.ToString() + " version: " + initialVersion.ToString()) 13
args.Cancel() 14
Else 15
_updateSuccess = True 16
End If 17
End Sub
In the above case the changes are discarded and a message is sent to the user if the object is already modified from another thread. You can replace it with your own strategy of collision handling.
Note: the supplied example has random delays to make the collision happen. You can experiment with the delay values to see different behavior.
01private void RandomWait() 02
{ 03
try 04
{ 05
Random r = new Random(); 06
Thread.Sleep((int)(5000 * r.Next(1))); 07
} 08
catch (Exception e) 09
{ 10
Console.WriteLine("Interrupted!"); 11
} 12
}
1Private Sub RandomWait() 2
Try 3
Dim r As Random = New Random 4
Dim sleepTime As Integer = 5000 * r.Next(1) 5
Thread.Sleep(sleepTime) 6
Catch e As Exception 7
Console.WriteLine("Interrupted!") 8
End Try 9
End Sub