Gallery2:Locking - Gallery Codex
Personal tools

Gallery2:Locking

From Gallery Codex

Locking in Gallery 2

Intended audience: This is a technical article intended for developers or other people that are interested in how locking works in Gallery 2.

What's Locking?

Gallery has to deal with concurrent access of multiple users on the same resources (albums, images, ...). What happens if two or more people want to edit the same item at the same time? Consider user X adding 50 items to an album that user Y is about to delete. User X starts the batch add process and user Y then deletes the album. Some weird errors could occur now since Gallery is supposed to add the remaing 30 items to an album that no longer exists.

Locking prevents such issues. Consider now a global manager that must be consulted before you can do anything.

  • User X would ask the manager whether items can be added to this album.
  • The manager first checks if someone else is already manipulating this album and then gives the ok to user X.
  • User X now starts the process of adding his items.
  • User Y asks the manager whether the album can be deleted now.
  • The manager remembers that user X is already doing something with this album and answers no to user Y.
  • User Y defers the deletion to a later time.

Thanks to the manager, the tasks of the two users didn't collide.

In the domain language of this problem, we say that user X requests a lock for item X from the manager. The manager itself can just be a global instance that checks whether there are currently already any locks requested for item X or not.

For a more detailed introduction to locking, see: http://www.javaworld.com/javaworld/jw-07-2000/jw-0714-locks.html

Read and Write Locking

In Gallery 2 you'll have to deal with hierarchical structures when locking. Each item or album possibly has a whole series of parent items. Since the all parent items are linked to filesystem folders, you want to ensure that they still exist if you do things like renaming or moving an item. That's when you need to read lock all the parent items of the item you move. And you want to write lock the item that you're actually changing.

Read Locking

  • If you're holding a read lock on an item, Gallery 2 guarantees you that the item is unchanged as long as you hold the lock.
  • Any number of users (threads) can hold a read lock for a specific item at the same time: Since there is no conflict of interest when multiple users want to view / read a specifc item, there's no reason to restrict a read lock on an item to a single user.
  • A read lock can be acquired as long noone holds already a write lock for that item.

Write Locking

  • If you're holding a write lock on an item, Gallery 2 guarantees you that noone else is readin or changing this item as long as you hold the lock.
  • Only one user can hold a write lock on an item at any time.
  • A write lock can be acquired only if noone else has any lock on the item in question. You can also acquire a write lock for an item that you already have read-locked.

The Locking API of G2

For a complete overview of the API, see: http://galleryproject.org/apidoc/GalleryCore/Classes/GalleryCoreApi.html

Acquiring a Lock

  • acquireReadLock($ids, $timeout=10): acquire a read lock for one or more items. (phpdoc)
  • acquireWriteLock($ids, $timeout=10): acquire a write lock for one or more items. (phpdoc)

And for convenience, there is also a method to read lock all the parents of a specific item:

  • acquireReadLockParents($id, $timeout=10): acquire read locks for all ancestors (parent, grandparent, ...) of a given item. (phpdoc)

Timeouts: Specifies how long the locking system should try to acquire the lock for you.

The methods to request a lock have an optional timeout parameter. If the locking system would just check if someone else already has a lock, we would end up with a lot of situations where a task cannot be processed because the requested resource is locked. This is why the locking system retries to acquire a lock for 10 seconds by default. You can decrease or increase this period with the optional parameter.

LockId: LockIds are not the same as entityIds (but entityIds and itemIds are the same since items are a subset of entities). Given a specific item, you acquire a lock and the lock system gives you a lockId which identifies this lock. When you want to refresh or release this lock, use the lockId and not the itemId.

Releasing Locks

After acquiring a lock, you must release the lock again once you no longer need it. Else you're unnecessarily blocking concurrent requests from viewing or editing albums.

  • releaseLocks($lockIds): release the given locks by lockId. (phpdoc)
  • releaseAllLocks(): release all locks that you have acquired in this HTTP request. This does not release locks of concurrent requests of course. (phpdoc)


Checking if an item is already locked by someone else

  • isReadLocked($id): check if an item is read-locked already. (phpdoc)
  • isWriteLocked($id): check if an item is write-locked already. (phpdoc)

Refreshing a Lock

By default, locks expire after 30 seconds. After that, they are no longer considered to be valid and they are ignored. But don't take this as a permission just to omit releaseLock(..) calls!

However, if you have e.g. a batch operation which needs to keep the parent album locked as long as the operation is in process, you can refresh the lock of the parent album periodically with the following method:

  • refreshLocks($freshUntil): refresh all your locks to extend their lifetime.

(phpdoc)

Why do we do that? The reason for this is that a developer might forget to release a lock or that the process even dies before abrubtely. To deal with forgotten locks, we expire all locks after a certain time.

Best Practices

When should you acquire a lock for an item? When do you also need to lock the parent album? And when is a read lock appropriate and when do you need a write lock? These questions are answered in this section.

When to acquire a write lock?

When you're changing an item, you must write lock it before you do the change.

When to acquire a read lock?

If you're changing something that references entity X, you should read lock X. Example: You're adding a comment to item X. Item X should be guaranteed to exist at least as long as you need it to which is during the creation of your comment. Thus you need to acquire a read lock for item X.

When to read lock the parent sequence of an item?

If a change is somehow linked to the filesystem, read lock the whole parent sequence of the item you're changing. That includes but is not limited to the following changes: rename item (the name, not the title), moving an item, deleting an item. Of course you also need to write lock the item that you want to change.

There are of course other cases that don't exactly match this criterion, but the same reasoning should help you to decide when to use what kind of locking or no locking at all.

When is no read lock necessary at all?

If you're just changing the timestamp, description or another attribute of an item which has no direct connection to something else, no read lock of a parent or so is needed at all. Just write lock the item that you're changing.

When to release a lock?

Gallery releases all locks at the end of a HTTP request. But you must still release locks as soon as possible to ensure that other requests are not waiting too long for you. Release the locks as soon as your changes are saved.

What timeout is appropriate?

How long should I wait for my lock? The default timeout of 10 seconds should be fine in most cases, no need to specify your own. If there's a good reason to keep waiting longer than that, set the timeout accordingly. But keep in mind that the user will be waiting in front of the computer for this period without any feedback.

What should I do if acquiring a lock fails?

That really depends on the specific case. You might want to show the user a specific error message in most cases because there's not a lot you can do about it. You can already set the retry timeout in the acquire[Read|Write]Lock() methods, so it doesn't make a lot of sense to retry it in your application code.

What API methods already do the locking for me?

A general rule of thumb: If you're using entity methods like $entity->delete(), you have to do the locking yourself. If you're using an API method like GalleryCoreApi::deleteEntityById($id), then locking is probably done for you. Better check than be sorry. You'll get an ERROR_LOCK_REQUIRED if you forgot to lock an item specifically, but we don't check during runtime if you really locked parent items and the such which can only be tested with concurrent requests.

An example

This example is from the ItemMove.inc controller in the core module. The task is to move a number of items from one album to another. Here's the code that is relevant for locking:

 /* First check if everything would be okay with the change */
 // ...check permissions, check for preconditions like no recursive moves, 
 //    load all entities. Just do all the harmless preparations before the
 //    actual move operation should be done.

Rule of thumb: The code within a acquireLock / releaseLock pair should be as compact / short as possible. You're blocking concurrent requests during this time and you have to do extra bookkeeping of acquired lockIds which is why this should be minimized.

 /* If no errors occured, acquire the necessary locks */
 /* 
  * Moving is a filesystem related change. Read lock the current parentsequence of the items that 
  * we move and do the same for their new parent album.
  */
 $lockIds = array();
 list ($ret, $lockIds[]) = GalleryCoreApi::acquireReadLockParents($oldParentId);
 if ($ret) {
   return array($ret, null);
 }
 list ($ret, $lockIds[]) = GalleryCoreApi::acquireReadLockParents($newParentId);
 if ($ret) {
   /* Release the already acquired locks ASAP on error. */
   GalleryCoreApi::releaseLocks($lockIds);
   return array($ret, null);
 }
 list ($ret, $lockIds[]) = GalleryCoreApi::acquireReadLock(array($oldParentId, $newParentId));
 if ($ret) {
   GalleryCoreApi::releaseLocks($lockIds);
   return array($ret, null);
 }

Rule of thumb: If there occurs an error after you acquired locks, release them as soon as possible before you return the program control to the calling instance by returning a error status object.

 /* Everything we depend on is read locked. Do the actual move. */
 /* 
  * Since ItemMove can move an arbitrary number of items, we have to think about how this code scales for large numbers of items. 
  * We should move it in batches and not all at the same time.
  */
 foreach ($seriesOfBatches as $batch) {
   /* Acquire the write lock for the items of this batch before moving them. */
   /* Write lock all the items we're moving */
   list ($ret, $currentLockIds) = GalleryCoreApi::acquireWriteLock($itemIdsOfCurrentBatch);
   if ($ret) {
     GalleryCoreApi::releaseLocks($lockIds);
     return array($ret, null);
   }
   /* Now we can finally do the move. */
   foreach ($itemIdsOfCurrentBatch as $selectedItem) {
     $ret = $selectedItem->move($newParentId);
     if ($ret) {
       GalleryCoreApi::releaseLocks(array_merge($lockIds, $currentLockIds));
       return array($ret, null);
     }
     $ret = $selectedItem->save();
     if ($ret) {
       GalleryCoreApi::releaseLocks(array_merge($lockIds, $currentLockIds));
       return array($ret, null);
     }
   }
   /* 
    * Now we can release the locks for the already moved items. But we keep the lockIds 
    * of the parent read locks since we still need them for the coming batches.
    */
   $ret = GalleryCoreApi::releaseLocks($itemIdsOfCurrentBatch);
   if ($ret) {
     GalleryCoreApi::releaseLocks($lockIds);
     return array($ret, null);
   }
   /* 
    * When doing batch operations, it's a good idea to have some checkPoints in between 
    * at safe points which commit the current transaction and open a new one. 
    */
   $ret = $storage->checkPoint();
 }
 /* And finally release the read locks of the parents once we're done with the operation. */
 $ret = GalleryCoreApi::releaseLocks($lockIds);
 if ($ret) {
   return array($ret, null);
 }
 /* Finish the task by redirecting to a view... */

Rule of thumb: Operations that involve locking an unknown or large number of items must be done in batches of a size of about maximally 100 items. The reason is that one of the locking system types of G2, the Flock based locking, works by opening a file for each item that you lock. So if you request locks for 2000 items, it will open 2000 files and keep them opened until you release the lock. This scales badly anyway, but the main reason to do this in batches of about 100 locks at a time is that unix / linux file systems keep track of how many file are opened by a specific process and there is a maximum of allowed open files per process. This maximum is configurable, and some webhosts have it set to a pretty low number. 100 seems to be a reasonable trade-off between performance (profit from doing things in batches) and compatiblity.

Locking and Transactions

Gallery 2 works with transactional databases. And generally, each user request is encapsulated in a single huge transaction which is committed at the end of the request just after Gallery 2 answers the HTTP request.

That means that concurrent requests don't take notice of anything of each other until the transaction has been committed. Which is exactly the opposite of what locking is about. When locking an item, we want to tell concurrent requests about it. Which is why locking is non-transactional in G2. As soon as you acquire a lock, other requests know about it.

Releasing locks is not non-transactional though. It's all handled for you, so don't worry. Just call the releaseLocks() method as soon as you no longer need a lock.

The details: In short: We cannot release a lock before the change for which we acquired the lock took effect. Since all changes in G2 are transactional, they don't take effect before the transaction they are in has been committed. If we released locks in a non-transactional manner, we would give up the lock before the change has been committed. Thus another request could do a colliding change in the time between released the lock and the transaction of the change has been committed. This may sound complicated and more like an academic case that never occurs, but with a popular Gallery or for long batch operatations, this is pretty important.

Database vs. Flock

  • Database based locking is very reliable.
  • Flock doesn't work reliably on Windows, but should work fine on Linux.
  • Flock is usually supposed to be faster than database based locking.
  • Flock isn't suitable for Gallery installations with multiple frontend servers that share a single database but use different filesytem disks for their local g2data folder.
  • Flock runs into filesystem limits (linux / unix ´ulimit -a´ shows the open files limit). Thus you need to do operations in batches smaller than this limit. G2 usually chooses batches of 100 items.
  • Database based locking is limited by the maximum query size which is usually considerably larger than the open files limit. But still, doing operations in batches is safer.