Working with Views

 

Author: Eike Stepper

This chapter covers view management, resource handling, querying, transactions, and related options in CDO client applications. Views are central to accessing and interacting with model data in a CDO repository. Understanding how to use views effectively is key to building responsive and scalable applications.

1  Understanding Views and Their Types

CDO provides several types of views, including read-only views and transactional views. Read-only views allow safe navigation of repository data without risk of modification, while transactional views enable changes and commits. This section explains the differences, use cases, and lifecycle of each view type.

2  Opening and Closing Views

Learn how to open views to access repository data and close them to release resources. Proper management of view lifecycles helps prevent memory leaks and ensures efficient resource usage.

3  Thread Safety

Views in CDO are inherently thread-safe, but this guarantee applies only to individual method calls. When performing multiple operations that need to be atomic or consistent, developers must use a CriticalSection to synchronize access to the view. A CDO view provides its critical section via the CDOView.sync().

Thread safety of CDO views is absolutely essential, even in single-threaded applications. The reason is that CDO views are accessed by background threads for tasks such as asynchronous updates, notifications, and event handling. If a view were not thread-safe, these background operations could lead to data corruption, inconsistent states, and unpredictable behavior in the application. By ensuring that views are thread-safe, CDO allows developers to build robust applications that can safely handle concurrent operations without risking data integrity.

This section discusses best practices for managing concurrent access, synchronizing operations, and avoiding race conditions when working with views in multi-threaded environments.

3.1  Using Critical Sections

To ensure thread-safe access to a CDO view when performing multiple operations, use the view's critical section object. It is returned by the CDOView.sync() method. The critical section provides methods to execute code blocks safely, such as CriticalSection.run(Runnable) and CriticalSection.call(Callable).

Here is an example of using a critical section with a callable to access multiple objects in a view atomically:

CrititicalSectionWithCallable.java      
CriticalSection sync = view.sync();

MyResult result = sync.call(() -> {
  // Access the view and its objects safely here.
  CDOObject object1 = view.getObject(id1);
  CDOObject object2 = view.getObject(id2);
  CDOObject object3 = view.getObject(id3);

  // Return a result object.
  return new MyResult();
});

The CriticalSection interface provides the following methods:

By default the critical section of a view uses the monitor lock of that view to synchronize. If you need a different locking strategy you can override this by calling CDOUtil.setNextViewLock(Lock) before opening the view.

Here's an example of setting a custom lock for the next view to be opened:

CustomLockForNextView.java      
Lock customLock = new ReentrantLock();
CDOUtil.setNextViewLock(customLock);

CDOView view = session.openView();
CriticalSection sync = view.sync();

if (sync instanceof LockedCriticalSection)
{
  Lock lock = ((LockedCriticalSection)sync).getLock();
  assert lock == customLock;
}
else
{
  throw new IllegalStateException();
}

The following chapter describes how to use a special kind of lock, a DelegableReentrantLock, that allows to delegate the lock ownership to a different thread.

3.2  Using a Delegable Lock

As an alternative to the default locking strategy of a view's critical section, you can use a DelegableReentrantLock, which allows to delegate the lock ownership to a different thread. This is useful in scenarios where you need to hold the lock while waiting for an asynchronous operation to complete in a different thread.

A typical example is the Display.syncExec() method in SWT/JFace UI applications. With the default locking strategy this can lead to deadlocks:

  1. Thread A (not the UI thread) holds the view lock and calls Display.syncExec() to execute some code in the UI thread.
  2. The Runnable passed to syncExec() is scheduled for execution in the UI thread. Thread A waits for the Runnable to complete.
  3. The UI thread executes the Runnable, which tries to access the view or an object of the view. This requires the view lock, which is already held by thread A.
  4. Deadlock: Thread A waits for the Runnable to complete and the UI thread waits for the view lock to be released.

Here is an example that illustrates this scenario:

DeadlockExample.java      
CDOObject object = view.getObject(id);
object.eAdapters().add(new AdapterImpl()
{
  @Override
  public void notifyChanged(Notification msg)
  {
    // This code is executed in a non-UI thread and holds the view lock.

    // The following call to Display.syncExec() will execute the Runnable in the UI thread
    // and make the current thread wait for it to complete. During that time the view lock
    // is still held by the current thread.
    Display.getDefault().syncExec(() -> {
      // This code is executed in the UI thread.
      // It tries to access the view while the view lock is held by the non-UI thread.
      // The result is a deadlock.
      CDOResource resource = view.getResource("/my/resource");
    });
  }
});

Note that, in this scenario, Thread A is holding the view lock while waiting for the Runnable to complete. This is kind of an anti-pattern, because it blocks other threads from accessing the view for an indeterminate amount of time. In addition, it is not necessary to hold the view lock while waiting for the Runnable to complete, because Thread A can not access the view in that time.

A DelegableReentrantLock can be used to avoid the deadlock. It uses so called lock delegation to temporarily transfer the ownership of the lock to a different thread. In the scenario described above, Thread A can delegate the lock ownership to the UI thread while waiting for the Runnable to complete. When the Runnable completes, the lock ownership is transferred back to Thread A. This way, the UI thread can access the view while executing the Runnable and no deadlock occurs.

DelegableReentrantLock uses DelegateDetectors to determine whether the current thread is allowed to delegate the lock ownership to a different thread. A DelegateDetector can be registered with the lock by calling DelegableReentrantLock.addDelegateDetector(DelegateDetector). The org.eclipse.net4j.util.ui plugin provides a DisplayDelegateDetector for the SWT/JFace UI thread that detects calls to Display.syncExec().

There are two ways to use a DelegableReentrantLock with a CDO view:

  1. Set the lock as the next view lock by calling CDOUtil.setNextViewLock(Lock) before opening the view. This way, the view will use the lock for its critical section. Here's an example:

    IndividualViewLock.java      
    CDOUtil.setNextViewLock(new DelegableReentrantLock());

    CDOView view = session.openView();
    CriticalSection sync = view.sync();

    // Acquire the view lock.
    sync.run(() -> {
      // This code is executed in a non-UI thread and holds the view lock.

      // The following call to Display.syncExec() will execute the Runnable in the UI thread
      Display.getDefault().syncExec(() -> {
        // This code is executed in the UI thread.
        // It can access the view because the lock ownership has been delegated to the UI thread.
        CDOResource resource = view.getResource("/my/resource");
        System.out.println("Resource: " + resource.getURI());
      });
    });

  2. Set delegableViewLockEnabled to true on the session. This way, all views opened from the session will use a DelegableReentrantLock for their critical section. The lock is created automatically and configured with all DelegateDetectors that are registered. Example:

    SetDelegableViewLockEnabled.java      
    session.options().setDelegableViewLockEnabled(true);

    CDOView view = session.openView();
    CriticalSection sync = view.sync();

    // Acquire the view lock.
    sync.run(() -> {
      // This code is executed in a non-UI thread and holds the view lock.

      // The following call to Display.syncExec() will execute the Runnable in the UI thread
      Display.getDefault().syncExec(() -> {
        // This code is executed in the UI thread.
        // It can access the view because the lock ownership has been delegated to the UI thread.
        CDOResource resource = view.getResource("/my/resource");
        System.out.println("Resource: " + resource.getURI());
      });
    });

4  Understanding the CDO File System

The CDO repository exposes a virtual file system for organizing model resources. This section describes the structure and usage of the file system, including root resources, folders, and different resource types.

4.1  The Root Resource

The root resource is the entry point to the CDO file system. It contains all top-level folders and resources, providing a hierarchical view of the repository's contents.

4.2  Resource Folders

Resource folders organize model resources into logical groups. Learn how to create, navigate, and manage folders to structure your repository effectively.

4.3  Model Resources

Model resources store EMF model data in the repository. This section explains how to load, save, and query model resources, and how they relate to EMF ResourceSet.

4.4  Binary Resources

Binary resources allow storage of non-model data, such as images or files, within the CDO repository. Learn how to manage binary resources and integrate them with your application.

4.5  Text Resources

Text resources store textual data, such as configuration files or documentation, in the repository. This section covers reading, writing, and organizing text resources.

5  Resource Sets and Their Usage

Resource sets are collections of resources managed together. Learn how to use EMF ResourceSet with CDO, manage resource lifecycles, and optimize performance for large models.

6  Navigating Models

This section provides techniques for traversing and querying model objects within views, including use of EMF APIs and CDO-specific features for efficient navigation.

7  Waiting For Updates

Learn how to synchronize your client with repository changes, block for updates, and react to notifications to keep your application state current.

8  Querying Resources

CDO supports querying resources using various criteria. This section explains how to construct and execute queries to locate resources and model objects efficiently.

9  Querying Model Objects

Learn how to query model objects using CDO's query APIs, including support for OCL, custom queries, and cross-references.

10  Querying Cross References

Cross references allow navigation between related model objects. This section covers techniques for querying and resolving cross references in CDO models.

11  Custom Queries

Extend CDO's querying capabilities with custom query implementations. Learn how to define, register, and execute custom queries for advanced use cases.

12  Units

Units are disjunct subtrees of model objects that can be managed independently. This section explains how to create, open, and close units, and how they can improve performance and consistency in your application.

13  View Events

Views emit events for changes, updates, and errors. This section explains how to listen for and handle view events to build responsive applications.

14  View Options

Configure view options to customize behavior, such as passive updates, notification handling, and more.

15  View Properties

Access and modify view properties to store custom metadata and configuration values.