Click here to Skip to main content
15,886,258 members
Articles / Programming Languages / C# 4.0

GC aware time-to-live cache

Rate me:
Please Sign up or sign in to vote.
5.00/5 (5 votes)
14 May 2015CPOL4 min read 11K   65   9   3
Want to learn how to get notifications on object-is-being-garbage-collected event? A volatility based decomposition - does it sound right?

Introduction

.Net 4 introduced a nifty class - a ConditionalWeakTable. It hides behind itself the ability to communicate to the garbage collector. In this article we will explore the hidden power of this communication.

Disclaimer:
This article is my personal exercise on the IDesign way of thinking, i.e. the volatility way of thinking.

Download TtlCaches.zip

What and Why

Let's imagine a system with clients talking to peers. Each message from a client to a peer requires a connection. Establishing a connection is a heavy operation. We want to cache open connections. A connection should be closed a while after all the messages of this specific connection are gone.

Here is a typical use case

  • Look for a cached connection to the peer
  • If a cached connection does not exist
    • Open connection and store it in a cache
  • Use the connection to send a message to the peer

However, it is not that easy to implement. To cache a connection we have to store it in a data structure, what is preventing it from been garbage collected. Thus we will need to maintain a reference counter per connection ourselves. To do this, we will have to expose the Get/Release methods, that will be consumed by a using directive with a IDisposable guard. The situation even worse if a message travels across multiple threads, since the using directive can't help us with threads.

Implementing all of the above just does not feel right. It feels like we are reinventing the garbage collector. Can't we just use the garbage collector .Net provides us?

So here is our goal: a GC aware time-to-live cache.

Design

Interface

C#
public interface ITtlCache<TFacet, TBody>
    where TFacet : class
    where TBody : class
{
    void Store(TFacet facet, TBody body);
    TBody[] GetDeadbodies(TimeSpan ttl);
}

Volatilities

The main purpose of the TtlCache is to encapsulate the interaction with the garbage collector and time-to-live concerns in such a way that it will not pollute the design and the implementation of our own cache component.

Let's list the volatilities that such a cache should encapsulate:

  • Non-intrusive programming model
    • No using and Get/Release methods
    • No weak references
    • No base classes
    • No events to be subscribed to
  • Interaction with garbage collector
    • Definitely cache has a tight interaction with GC, it should be encapsulated
  • Asynchronous nature of garbage collector
    • GC collects objects in its own threads. Any GC notification would be delivered in its arbitrary thread. That GC thread should be encapsulated.
  • Reference counting
    • It is obvious that there is some reference counting mechanism in the cache. It should be encapsulated.

There is another volatility that might have been encapsulated. This is the race condition of putting to the cache and releasing a dead object. But after thinking about it, I was finally convinced that this volatility belongs to a outer scope of a time-to-live cache.

Components static view

Here is the static view of the TtlCache ingredients (participants). The encapsulated volatilities are listed near each participant.

Image 1

The TtlCache may be thought as a kind of map that maps a facet (lightweight object) to a body (heavy object). In our esoteric system a message is a facet to a connection. The TtlCache wraps an object with a TtlItem and stores it in a private repository ItemsRepo. Then the TtlCache creates a DeathNotifier for each pair of the facet and the body. The DeathNotifier delegates the facet-is-garbage-collected event to the TtlItem where reference counter is maintained.

Threads

Methods of the TtlCache are thread safe and can be called within an arbitrary thread. Since theTtlCache does not expose any events, its clients never deal with garbage collector threads either. The state of the TtlCache is stored in the ItemsRepo class which is protected by an exclusive lock.

Image 2

Usage example

Here is a basic unit test

C#
class Message {}
class Connection {}

[Test]
public void Item_should_be_removed_after_its_death()
{
    var cache = TtlCache<Message, Connection>.New();

    var connection = new Connection();
    var message = new Message();

    cache.Store(message, connection);

    message = null;

    GC.Collect();
    GC.WaitForPendingFinalizers();
    Thread.Sleep(10.Milliseconds());

    var bodies = cache.GetDeadBodies(1.Milliseconds());

    Assert.That(bodies[0], Is.SameAs(connection));
}

How does it work

The essential part of the implementation is the .Net core class ConditionalWeakTable. It was added to .Net 4 to help compilers to store custom properties per object. The ConditionalWeakTable gives us a possibility to associate any property (object) with a peer (another object) in such a way, that when the peer is collected by the garbage collector, the associated property is collected as well. The central idea of the implementation is to associate with a given object a DeathNotifier class with a destructor. Thus when the given object is collected by the garbage collector the associated death-notifier is collected as well, and its destructor is called. So the destructor effectively becomes a the-given-object-is-collected event.

Image 3

Here is the DeathNotifier essence.

C#
class DeathNotifier
{
    private IReferenceCounter _reference;

    ~DeathNotifier()
    {
        var reference = _reference;

        if (reference != null)
            reference.ReleaseBody();
    }
}

Any given object is wrapped by an internal TtlItem that implements two interfaces ITtlItem<TBody> and IReferenceCounter.

C#
interface ITtlItem<TBody>
    where TBody : class
{
    TBody Body { get; }
    DateTime DeadSince { get; }
}
C#
interface IReferenceCounter
{
    void LinkBody();
    void ReleaseBody();
}

Here is how a TtlItem implements a reference counting and a dead-since logic.

C#
class TtlItem<TBody> : ITtlItem<TBody>, IReferenceCounter
    where TBody : class
{
    void IReferenceCounter.LinkBody()
    {
        int count = Interlocked.Increment(ref _referenceCounter);

        if (count == 1)
            _deadSince = DateTime.MaxValue;
    }

    void IReferenceCounter.ReleaseBody()
    {
        int count = Interlocked.Decrement(ref _referenceCounter);

        if (count == 0)
            _deadSince = DateTime.UtcNow;
    }
}

Use cases

Image 4


Image 5

Summary

As we said before the main purpose of the TtlCache is encapsulating of the garbage collector and time-to-live concerns.

With the TtlCache you are free to implement your own cache component according to any logic required by your design. The TtlCache provides you with garbage collector communication. You just add pairs of facet-to-body to it and ask when the bodies may be disposed. This way you are free to implement the clean-up logic the way you prefer. It may be an asynchronous periodically scheduled task, or a synchronous maintenance each time a connection is created.

The TtlCache interface is simple

  • Store to the cache
  • Ask the cache for dead bodies

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Chief Technology Officer Cpp2Mtl Integration Solutions
United States United States
My real name is Reuven Bass. My first article here was published under the Mamasha Knows pseudonym. It worked. So, I stay with Mamasha for a while. (If it works - do not touch it)

Programming became my life from thirteen. I love coding. I love beauty. I always try to combine coding and beauty.

RB

Comments and Discussions

 
Question+5 ... Where is source? - Controller or Action Not Found ... Pin
RAND 45586615-May-15 19:27
RAND 45586615-May-15 19:27 
AnswerRe: +5 ... Where is source? - Controller or Action Not Found ... Pin
Reuven Bass17-May-15 9:55
Reuven Bass17-May-15 9:55 
GeneralRe: +5 Pin
RAND 45586618-May-15 3:57
RAND 45586618-May-15 3:57 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.