2009/04/23

Understanding ActiveRecord Sessions 2

This article series deals with the internal session keeping of Castle ActiveRecord. This installment introduces the concept of a scope and how scopes are used within ActiveRecord.

Introducing Scopes

Having a session per database call is suboptimal. There is no chance to use transactions, which is a showstopper for most serious uses. Another issue is lazy loading: Lazy loading prevents NHibernate from loading large collections until they are requested, avoiding to load unnecessary data. Lazy loading is however only possible when done within a single session.

Castle ActiveRecord uses scopes to overcome this. A scope represents a single unit of work. It is not necessarily a single transaction, but may span multiple transactions. Prior to NHibernate 2.0, when transactions were not mandatory, scopes did not even have to support transactions.

The scopes that are part of the ActiveRecord package do always share a single session that is used for all persistance related calls. However, it is possible to implement a scope that uses a new session, but the changed semantic should be clearly communicated in such a case.

Localizing Scopes

In order to access scopes without holding a reference to the object, they stored in thread static stacks. The central interface for this is IThreadScopeInfo, which is implemented by various classes to cover different situations such as web applications. WebThreadScopeInfo for example uses a HttpContext for storing the scopes while the default ThreadScopeInfo implementation uses a thread static field.

In order to acquire the current IThreadScopeInfo implementation, ThreadScopeAccessor can be used. This class is a singleton providing the current ScopeInfo under

public IThreadScopeInfo ScopeInfo

Additionally it acts a proxy to this scope, delegating calls to the scope info in the property above. This allows to access the current scope without keeping a field for it using

ThreadScopeAccessor.Instance.XXX();

where XXX is one of the methods defined in IThreadScopeInfo.

With the IThreadScopeInfo available, the current scope can be requested by SessionFactoryHolder using the following methods:

ISessionScope GetRegisteredScope()
bool HasInitializedScope

Scope Initialization

When a scope is created, it has no sessions stored at first. That is intentional. While the ActiveRecordStarter configures the ISessionFactoryHolder at startup, it cannot configure an arbitrary number of ISessionScope implementations.

Therefore the initialization of scopes is implemented using a simple protocol between ISessionFactoryHolder and ISessionScope:

  1. Upon creation, the ISessionScope registers itself with the current IThreadScopeInfo.

    void IThreadScopeInfo.RegisterScope(ISessionScope scope)
  2. ISessionFactoryHolder fetches the scope the next time it has to deliver a session to the ActiveRecordBase methods.

    bool IThreadScopeInfo.HasInitializedScope
    ISessionScope IThreadScopeInfo.GetRegisteredScope()
  3. The ISessionFactoryHolder asks the ISessionScope whether it already has a suitable session stored. Scopes do not hold only one session. Due to different database connections by root type, they need to hold a dictionary of sessions. The scope doesn't know the key to the sessions because the key is provided by the ISessionFactoryHolder everytime a session is required.

    bool ISessionScope.IsKeyKnown(object key)
  4. If there is a session registered with the scope, the session is requested and the protocol ends.

    ISession ISessionScope.GetSession(object key)
  5. If there is no suitable session available the ISessionFactoryHolder asks the ISessionScope whether it accepts an existing session or if it wants to create its own session.

    bool ISessionScope.WantsToCreateTheSession
  6. Depending on the answer to 5, the ISessionFactoryHolder either opens a session itself or provides a suitable ISessionFactory to the ISessionScope so that the session can be created by the scope itself.

    ISession ISessionScope.OpenSession(ISessionFactory sessionFactory, IInterceptor interceptor)
  7. The session is registered with the ISessionScope by the holder. This is done regardless who in fact created the session.

    void ISessionScope.RegisterSession(object key, ISession session)
  8. The freshly registered session is fetched from the scope.

    ISession ISessionScope.GetSession(object key)

The different Scope Types

Scopes generelly have two attributes that describe there behaviour: by FlushAction and by SessionScopeType. The FlushAction controls whether changes are automatically flushed to the database. The SessionScopeType describes the behaviour of the scope in common, most important whether the scope supports transactions.

If the FlushAction is defined as FlushAction.Never, no writing to the database will occur unless ISessionScope.Flush() is called by the using code. This behaviour gives full control to the using code, but requires it to control flushing. This is important: If the changes are not flushed, queries will still find an older state in the database although the entity has already changed.

FlushAction.Auto instructs the NHibernate session to flush its first-level-cache whenever needed. That means that if the cache contains an unflushed change to an entity, the session will write those changes back before it runs a query against the entity's type.

The SessionScopeType has four values:

SessionScopeType.Undefined
SessionScopeType.Simple
SessionScopeType.Transactional
SessionScopeType.Custom

Undefined should never be used; it is more an error state than a valid scope type. Simple and Transactional define whether the scope supports transactional behaviour. Custom can be used for own implementations that fall in between. An example would be a SessionScope that uses transactions only for specific sessions.

The next part of the series will show how ActiveRecord usage differs when using a scope.

2009/04/21

Understanding ActiveRecord Sessions 1

This article series deals with the internal session keeping of Castle ActiveRecord. It begins in explaining how sessions are managed within ActiveRecord and how the user can influence this behavior.

What happens when Save() is called?

The first part of our journey starts in ActiveRecordBase. Whenever a method is called that requires a NHibernate session, it requests one from the following field:

protected internal static ISessionFactoryHolder holder;

Since that field is marked protected internal static, subclasses of ActiveRecordBase can directly access it to acquire a session. This is done with the following methods:

ISession CreateSession(Type type);
void ReleaseSession(ISession session);
void FailSession(ISession session);

The first method requests a session from the ISessionFactoryHolder, that can be used to issue calls to the session, such as Save(), Load() etc. After the operation was completed, ReleaseSession must be called, preferably in a finally block.

If an exception is raised from NHibernate, the session cannot be used anymore. The ISessionFactoryHolder must be notified through the FailSession-method.

To further understand the inner workings of ActiveRecord, we need to look at the default implementation of ISessionFactoryHolder, which is simply called SessionFactoryHolder.

The SessionFactoryHolder keeps a dictionary which maps the configured ActiveRecord root types to ISessionFactory instances. Whenever CreateSession is called, it fetches the ISessionFactory for that type and creates an ISession.

When ReleaseSession is called the session is flushed and disposed, closing any open connections. If you use the holder to acquire sessions directly, keep in mind that it is necessary to release them or you will get a resource leak. Database sessions are among the most critical resources with regard to leaks.

If an exception is raised, the FailSession method will clear the session so that it doesn't throw again when the session is released.

When ActiveRecord is used without any scopes, all of this happens on a single call of any of the data retrieval or persistence methods (Save(), FindAll() etc.):

  1. ActiveRecordBase gets an ISession from the SessionFactoryHolder. ActiveRecordMediator simpy calls a static method on ActiveRecordBase, so there are no differences with respect to session handling.
  2. ActiveRecordBase performs the desired database operation.
  3. If there is no exception, ReleaseSession is called by ActiveRecordBase. In case of an exception, FailSession is called instead and the exception is rethrown.
  4. SessionFactoryHolder closes the ISession instance. Upon the next call, a new session is created.

But what if we need to have a single session span multiple commands? This will be handled in the following articles when we talk about scopes.