Fork me on GitHub

Jandal Service Synchronisation

How to use Jandal's locking mechanism to control concurrent access to Services

P

reviously in Services I introduced Services with a basic example which provided read-only access to a collection of poems. In Jandal, the same instance of a ServiceSet is shared by all executing instances of the Application that it has been supplied to. By default, the Services are able to be accessed concurrently by the Applications. This can lead to concurrent modification problems in Services that allow both read and write access to data: one Application can select a record to update, but just before it saves it, another Application could potentially delete it. In this article I'm going to show you how you can ensure that each Application instance gets exclusive access to a Service for as long as it needs to, using Jandal's Service-locking mechanism.

Example Application: AddressBook

AddressBook is a Jandal example application that , you guessed it, allows you to create, edit and delete addresses.

Sourcecode

AddressBookController.java

Shown below is the AddressBookController from theAddressBook application. This controller switches the application between its browsing and managing states. When browsing, it lists the existing addresses. You can then switch it to its managing state, in which you can list, create, delete and update addresses. The managing state has a child AddressBookManagementController which performs those functions.

Obviously, I want to synchronize access to the address storage service to prevent the kind of race condition I mentioned above. However, I also want to allow people to be able to fire up the application at any time and browse the address list, even while the list is being managed.

To accomplish this, I split address storage into two services: AddressBrowsingService, an unsynchronized read-only service that provides the address list, and AddressManagementService, a synchronized service which provides the list, create, delete and update functions. Both of these share the same data access object (DAO), on which all methods are synchronized (using the Java modifier), as you'll see soon.

When the controller enters its browsing state, it fetches itself the AddressBrowsingService and copies the address list from it to an output. The controller is able to get that service without locking it, because it is not synchronized.

Then when we fire a manage view event at the controller, it transitions to managing. The first thing the controller does in that state is try to lock the AddressManagementService within 1000 milliseconds. If it fails, it then transitions to managementBusy. This happens when another Application is already in the managing state, holding the AddressManagementService. We can fire a manage event again when we suspect that the AddressManagementService has become available again, to once more attempt to enter the managing state. Note that I had to define the managementBusy state before the managing state. Recall that a State's onEntry method executes as soon as the State is added to its controller. As soon as managing is entered, it needs managementBusy to exist in case it needs to transition into it.

When it has a lock on the AddressManagementService, the managing state adds an AddressBookManagementController to itself, which starts up and provides the address list management functions. Since it never explicitly releases the lock at any time, the managing state holds the lock for as long as it is active, implicitly releasing it when it is transitioned out of. For its entire lifetime, the AddressBookManagementController therefore has exclusive access to the AddressManagementService, thanks to its parent managing state holding the lock for it.

public class AddressBookController extends Controller {
    public AddressBookController() throws JandalCoreException {
        super("addressBookController");
    }

    protected void onStart() throws JandalCoreException {
        addInitialState(new State("browsing") {
            protected void onEntry() throws JandalCoreException {

                setOutput("template", "browsing.ftl");

                AddressBrowsingService abs =
                        (AddressBrowsingService) getService(AddressBrowsingService.class.getName());

                setOutput("addressList", abs.getAddresses());

                addViewEventProcessor(new EventProcessor("manage") {
                    protected void onEvent() throws JandalCoreException {
                        doTransition("managing");
                    }
                });
            }
        });

        addState(new State("managementBusy") {
            protected void onEntry() throws JandalCoreException {

                setOutput("template", "busy.ftl");

                addViewEventProcessor(new EventProcessor("browse") {
                    protected void onEvent() throws JandalCoreException {
                        doTransition("browsing");
                    }
                });

                addViewEventProcessor(new EventProcessor("manage") {
                    protected void onEvent() throws JandalCoreException {
                        doTransition("managing");
                    }
                });
            }
       });

       addState(new State("managing") {
           protected void onEntry() throws JandalCoreException {
               if (lockService(AddressManagementService.class.getName(), 1000L) == null) {
                   doTransition("managementBusy");
                   return;
               }

               setOutput("template", "managing.ftl");

               addChildController(new AddressBookManagementController());

               addViewEventProcessor(new EventProcessor("browse") {
                    protected void onEvent() throws JandalCoreException {
                        doTransition("browsing");
                    }
               });
           }
       });
    }
}

AddressBookServiceSet.java

Here's the ServiceSet layer for the application, with the two services sharing the same DAO.

public class AddressBookServiceSet extends ServiceSet {

    public AddressBookServiceSet() {
        AddressDao dao = new AddressDao();
        this.addService(new AddressBrowsingService(dao));
        this.addService(new AddressManagementService(dao));
    }
}

AddressDao.java

Here's the DAO. Notice the synchronization of each method. This ensures that an AddressBrowsingService and an AddressManagementService, operating concurrently, don't tread on each other's toes when using it.

public class AddressDao {
   
    public synchronized Address addAddress(String name, String email, String url) { ...  }

    public synchronized Address getAddress(String id) throws Exception { ... }

    public synchronized Address updateAddress(String id, String name, String email, String url) throws Exception { ... }

    public synchronized Address deleteAddress(String id) throws Exception { ... }

    public synchronized List getAddresses() { ... }
}

AddressBrowsingService.java

Here's the address browsing service. It basically wraps the DAO's address list query function. Recall that I don't need to synchronize this, so in its constructor I flag it as not synchronized. Jandal will not require it to be locked before use, so multiple applications can use it concurrently.

public class AddressBrowsingService extends Service {
    public AddressBrowsingService(AddressDao dao) {
        this.dao = dao;
        this.setSynchronized(false);
    }

    public List getAddresses() {
        return dao.getAddresses();
    }

    private AddressDao dao;
}

AddressManagementService

Here's the address management service. This guy is to subject to race conditions if used concurrently by multiple applications, so in its constructor I'm flagging it as synchronized. Jandal will then require that it be locked before use.

public class AddressManagementService extends Service {
    public AddressManagementService(AddressDao dao) {
        this.dao = dao;
        this.setSynchronized(true);
    }

    public Address addAddress(String name, String email, String url) {
        return dao.addAddress(name, email, url);
    }

    public Address getAddress(String id) throws Exception {
        return dao.getAddress(id);
    }

    public Address updateAddress(String id, String name, String email,
        String url) throws Exception {
        return dao.updateAddress(id, name, email, url);
    }

    public Address deleteAddress(String id) throws Exception {
        return dao.deleteAddress(id);
    }

    public List getAddresses() {
        return dao.getAddresses();
    }

    private AddressDao dao;
}

Conclusion

I've shown you a technique for locking a Service, where a State locks it within its onEnter method for exclusive access by a child Controller. You can also lock them from within the onStart method of a Controller, and the onEvent method of an EventProcessor; each of these elements has lockService, getService and unlockService methods.

Unless explicitly released with unlockService, any lock acquired in these elements is automatically released when the element expires. In this case, the lock is released when the State is exited.

You can only explicitly release a lock in the same element that you acquired it. So in this example, the child Controller is not allowed to release the lock acquired by the parent State, because it is the State that owns the lock. One way to look at it is that the State also "owns" the Controller, and allows it to access the Service it has locked.

So Jandal gives you a choice of three scopes for your Service locks: Controller, State or EventProcessor.