Server-Side Components
Because the server code is language independent, I won't detail the server-side implementation. I do examine how the server handles the client's AJAX requests, but more from a higher level using pseudocode. Still, it's important to understand the server's roles.
We develop in Java and have our own homegrown Struts-like framework. In this framework, actions that users take on the Web are delivered through a controlling servlet to the framework as URLs. The path portion of the URL is parsed apart, mapped first to a particular application, and then within that application to an implementation of an action, where the real server-side work is done. This server-side action is a Java type that the framework looks up, instantiates if necessary, and invokes the lifecycle. The lifecycle of a server-side action handles the user request in some wayusing the query portion of the URLto render content that can be used to write a response to a client. The server-side action lifecycle API contains this central method:
public Page execute(UrlArguments args, Person person)
There is an implementation of this interface on the server that responds to the AJAX lock request running in the window.setTimeout loop. Included in the URL arguments is the nature of the client's lock request, whether for obtaining a lock, releasing a lock, or querying the status of a lockand the ID of the record being used. This server-side component can be handling numerous client requests. There will be a request for every client page, coming in at the rate of the timer in the window.setTimeout loop. If there are only 10 clients and the call is made every 10 seconds per client, the server will be handling 60 requests per minute. Therefore, this code must run quickly, and in our implementation it mostly maintains a hash map from RecordId to RecordLock. Requests to obtain locks insert RecordLocks into the map; requests to free locks remove them; and status requests check for their existence.
Clients can make three kinds of requests of the server:
- status. Ask if record is locked or not.
- lock. Request to obtain lock on record.
- unlock. Request to free lock on record.
A record's status is locked or unlocked. Because all clients make requests for lock status, the owner of a lock is either the current user or someone else. Also, it's possible for a lock owner to let a session time out; say a user locks a record for editing, then goes for a two-hour lunch. To accommodate all these different states, the server sends back one of four state messages to the client:
- locked. Another user owns the lock on this record.
- unlocked. No user owns the lock on this record.
- owned. Current user owns the lock on this record.
- noSession. Current user's session timed out.
For every lock status request a client makes, the server is given the ID of a record and the person making the request. Requesters may or may not be the owner of the lock. Our applications are configured to hold a map with record IDs as keys and RecordLocks as values, and to track whether a user's session is still valid. The pseudocode in Listing One is the logic of our server-side handling of client lock requests.
public Page execute(UrlArguments args, Person requester) { recordID = args.getValue("recordID"); recordLock = map.get(recordID); // try to get lock for record lockOwner = requester; // changes if lock owned by someone else locked = "unlocked"; // changes based on actual lock state // Requesting status of lock for a given record if(request_is_for_lock_status) { if(recordLock != null) { // Since client can continue to poll server, // even after session has timed out, // this will stop the loop from running on the client. if(app.userSessionExpired(recordLock.getOwner())) { locked = "noSession"; } // Record is locked by someone else { // Request was from current lock owner if(recordLock.getOwner() == requester) { locked = "owned"; } // request by someone other than owner else { lockOwner = recordLock.getOwner(); locked = "locked"; } } } } // Requesting a lock for a given record else if(request_is_for_lock) { // Record already locked by the current requester if(recordLock != null && recordLock.getOwner() == requester) { locked = "owned"; } // Record is owned/locked by someone else { locked = "locked"; lockOwner = recordLock.getOwner(); } // Not owned or locked already, so lock it else if(recordLock == null) { map.add(recordID,requester); locked = "owned"; } } // Else, unlocking else { // Should never get a request to unlock a record // which isn't already locked Assert(recordLock != null); map.remove(recordID); locked = "unlocked"; } return PageLockPage(lockOwner, locked); }