Synch Utils Module

Introduced in 4.1.x

Introducing the Synch Utils Module

The Synch Utils Module is a powerful DMSContainer built-in job provided in “the box” - it allows to synchronize process or system wich runs on different machines easily with a intuitive and effective interface. It acts as a superpowered distributed lock.

The SynchUtils module introduce the concept of “Exclusive Lock” in DMSContainer.

The exclusive lock is used to implement distributed Pessimistic Offline Locking design pattern.

As stated by the author David Rice:

Pessimistic Offline Locking prevents conflicts between concurrent business transactions by allowing only one business transaction at a time to access data.

As mentioned in the design pattern description, this exclusive lock prevents the associated shared resource from being accessed in unwanted ways. This lock mode is usually obtained to modify data. The first client which lock a resource exclusively is the only process that can alter the resource until the exclusive lock is released. Just like OS Critical Sections, this approach works only if all the client interested in altering the shared resource actually use the lock. The lock itself doesn’t know anything about the resource to which it refers, it’s just a “name” which is conventionally used to lock something.

Lock Identifiers

A lock identifier (the lock name) it’s just a string anche can contains only the following characters:
'a'..'z','A'..'Z','0'..'9','_',':','.','-' (where .. denotes an interval between the characters).

All the following are valid lock identifiers:

lock1
lock-customers-123
lock-invoice-321
invoice-123
:invoice:321:details:
user.daniele
USERS.DANIELE
resource:1.2.3

Working with exclusive locks

The basic scenario of exclusive locks workflow is quite simple, as shown in the next piece of pseudocode.

//To get the lock, we need to call try_acquire_lock method with all the required parameters
lock_handle = try_acquire_lock(<params>)
if (lock_handle == 'error'){
    raise Error("cannot get the lock")
}
//the lock_handle is required for all subsequent operations on this lock

while (not work_is_done) {
    do_stuff()
}    

//we terminated the work on the shared resource, can release the lock
release_lock(lock_handle)

Remember, to extend or release the lock you need to have the lock_handle returned by the try_acquire_lock method.

The following diagram explain a simple, but real, scenario.

Scenario 1: basic synchronization with expiring lock

Scenario 2: synchronization with lock extra data

SynchUtils APIs reference

TryAcquireLock

function TryAcquireLock(const Token: string; const LockIdentifier: string; const LockTTL: Int64; const LockData: TJsonObject): string

Tries to acquire an exclusive lock on LockIdentifier for a max of LockTTL minutes, optionally attaching the data contained in LockData. TryAcquireLock calls cannot be nested - a subsequent call with same LockIdentifier tryes to re-acquire the lock and will raise an exception if can’t. If the lock has been correctly acquired, TryAcquireLock returns a LockHandle (which is a random string) that need to be used to extend or release the obtained lock. If TryAcquireLock cannot acquire lock, returns the const string error.

ExtendLock

function ExtendLock(const Token: string; const LockTTL: Int64; const LockHandle: string): Boolean;

Allows to extend the LockTTL for an owned-lock. LockExtender is the token returned by the TryAcquireLock call. Lock extension starts from “now” for LockTTL seconds.

GetLockData

function GetLockData(const Token: string; const LockIdentifier: string): TJDOJsonObject;

Get the lock data from an exclusive lock (owned, or not owned, by the user). If the lock doesn’t exist, an exception raise.

GetLockExpiration

function GetLockExpiration(const Token: string; const LockIdentifier: string): Int64;

Returns the time remaining for the natural exclusive lock expiration (in seconds) and the LockData. If the lock doesn’t exists raises an exception.

GetExclusiveLockQueueName

function GetExclusiveLockQueueName(const Token: string; const LockIdentifier: string): TJDOJsonObject;

Returns the system queue where any change to the lock status is published. To read this queue you need to be an user allowed to event_read role.

ReleaseLock

function ReleaseLock(const Token: string; const LockHandle: string): Boolean;

Release an owned exclusive lock and return true. If the lock doesn’t exist or is not owned by the current user, return false.

GetLocks

function GetLocks(const Token: string): TJDOJsonObject;

Returns all active locks - only ADMIN and MONITOR roles can call this method.

Exclusive Locks Example

This code is from an official example published in the samples repository

To use this example launch two or more instances it with the following comman lines:

C:\>python write.py daniele

C:\>python write.py peter

C:\>python write.py bruce

C:\>python write.py scott

This script requires the python proxy. Here’s the full sample code.

# file: writer.py

import sys
import os
import time
import random
from os.path import dirname
sys.path.append(dirname(dirname(__file__))) #allows to use commonspy folder

from commonspy.synchutilsproxy import SynchUtilsRPCProxy, SynchUtilsRPCException

if len(sys.argv) != 2:
    raise Exception("Invalid writer's name")
name = sys.argv[1]
proxy = SynchUtilsRPCProxy('https://localhost/synchutilsrpc')
res = proxy.login("user_admin","pwd1")
token = res["token"]
print(f"My name is {name}")
print("Using " + res['dmscontainerversion'])

lock_identifier = "fileres1"
try:
    while True:
        try:
            while True:
                lockhandle = proxy.try_acquire_lock(token, lock_identifier, 20, {})    
                if lockhandle == 'error': 
                    print("Cannot acquire the lock... let's wait...")
                    time.sleep(1 + random.random() * 2)
                    continue
                print("Lock acquired, writing the file...")
                with open("file.log","a") as f:
                    f.write(f"My name is {name:10s}, and I've got the lock identifier {lock_identifier}, my current handle is {lockhandle}\n")
                    time.sleep(0.2 + random.random() * 2)
                print("Done, let's release the lock")
                proxy.release_lock(token, lockhandle)
                lockhandle = ""
                time.sleep(1 + random.random() * 2)
        except SynchUtilsRPCException:
            res = proxy.login("user_admin","pwd1")
            token = res["token"]
except KeyboardInterrupt:
    print("Quit")

Running multiple instances you can get an idea of SynchUtils module power. Let’s say you launched 4 instances - these 4 instances use a remote SynchModule lock to synchronize their access to a local file. Any process can work on the file without interfere with the other because of the lock. The same behavior is achived for remote processes spread over the globe, or just in your LAN.