Przezroczysta pula obiektów w Autofac

Koniec urlopu, bieżące tematy ogarnięte, więc pora na kolejny wpis.

Ostatnio natrafiłem na problem ograniczonych dostępnych zasobów, które mogą być używane równolegle w środowisku o asynchronicznej specyfice wywołań. Dokładniej sytuacja wyglądała w ten sposób, że pierwotnie zasób zewnętrzny był jeden i w aplikacji był reprezentowany przez interfejs, powiedzmy IResource. Szybko okazało się, że wydajność pojedynczego zasobu jest niewystarczająca i trzeba dołożyć dwa kolejne.

Ze względu na fakt że zasób był w kontenerze aplikacji rejestrowany jako “SingleInstance”, oraz przechowywał stan (zewnętrzny komponent), pojawił się problem dostarczenia dla każdego wywołania asynchronicznego dokładnie jednej instancji IResource, w taki sposób aby wszystkie obiekty w ramach jednego wątku otrzymały tą samą instancję oraz problem jednoczesnej obsługi wywołań ograniczonej przez ilość zewnętrznych zasobów reprezentowanych przez IResource.

Skalowanie systemu o nowe instancje IResource powinno być przezroczyste, a obiekty używające IResource nie powinny mieć wiedzy o tym, że pochodzi on z puli lub jest SingleInstance. Z tego powodu zarządzaniem pulą obiektów powinien zajmować się kontener aplikacji, w sposób niewidoczny w implementacji obiektów używających IResource.

Poniżej przedstawię w jaki sposób poradziłem sobie z powyższym problemem.

Realizację rozpocząłem od implementacji klasy generycznej ObjectPool<T>, gdzie T oznacza typ zarządzanego obiektu.

public class ObjectPool<T>
{
    protected readonly Stack<T> PooledObjects;
    protected readonly HashSet<T> LockedObjects;
    protected object PoolModificationMutex = new object();
    protected object PoolGettingMutex = new object();

    private readonly AutoResetEvent autoResetEvent = new AutoResetEvent(false);

    public ObjectPool(IEnumerable<T> pooledObjects)
    {
        LockedObjects = new HashSet<T>();
        PooledObjects = new Stack<T>(pooledObjects);
    }

    public T GetElement()
    {
        lock (PoolGettingMutex)
        {
            if (!HasFreeObject())
                WaitForFreeObject();

            lock (PoolModificationMutex)
            {
                EnsureNextThreadWillWaitForObject();

                var element = PooledObjects.Pop();
                LockedObjects.Add(element);

                return element;
            }
        }
    }

    private void EnsureNextThreadWillWaitForObject()
    {
        autoResetEvent.Reset();
    }

    private void WaitForFreeObject()
    {
        autoResetEvent.WaitOne();
    }

    public void FreeElement(T element)
    {
        lock (PoolModificationMutex)
        {
            LockedObjects.Remove(element);
            PooledObjects.Push(element);

            NoticeFreedObject();
        }
    }

    private void NoticeFreedObject()
    {
        autoResetEvent.Set();
    }

    public bool HasFreeObject()
    {
        return PooledObjects.Any();
    }
}

Kolejnym krokiem było zaimplementowanie właściwej dla IResoure implementacji puli: IResourcePool

public interface IResourcePool
{
    IResource GetResource();
}
public class ResourcePool : IResourcePool
{
    private readonly ObjectPool<IResource> resourcePool;
    private readonly Dictionary<IResource, IResourceConfig> resourcePoolConfigs;

    public ResourcePool(IResourcePoolConfig config)
    {
        resourcePoolConfigs = config.Services.ToDictionary(resourceServerConfig => (IResource)new Resource(resourceServerConfig), resourceServerConfig => resourceServerConfig);
        resourcePool = new ObjectPool<IResource>(resourcePoolConfigs.Select(resource => resource.Key));
    }

    public IResource GetResource()
    {
        Resource resource = resourcePool.GetElement();
        return new PooledResource(FreeResource, resource);
    }

    private void FreeResource(Resource resource)
    {
        resourcePool.FreeElement(resource);
    }
}

Klasa ta otrzymuje z konstruktora obiekt konfiguracyjny, dostarczający listę dostępnych konfiguracji dla wszystkich instancji zasobu i dla każdej konfiguracji tworzy nowy obiekt w puli. Dodatkowo zwrócić uwagę trzeba, że z puli podczas pobierania obiektu nie jest zwracany obiekt trzymany w puli, ale jest tworzony dekorator PooledResource otrzymujący w konstruktorze delegat do funkcji zwalniania obiektu w kontenerze.

public class PooledResource : IResource, IDisposable
{
    private readonly Action<IResource> freeResource;
    private readonly IResource resource;

    public PooledResource(Action<IResource> freeResource, IResource resource)
    {
        this.freeResource = freeResource;
        this.resource = resource;
    }
}

Dzięki takiemu zabiegowi, nie musimy się martwić, zwracaniem obiektu do puli. Zajmuje się tym GC.

W ten sposób mamy zapewnione, że gdy obiekt z puli przestanie być potrzebny, zostanie do niej zwrócony.

Ostatnią rzeczą, którą trzeba zrobić to zarejestrować naszą pulę w Autofac, tak aby było to przezroczyste dla programisty.

builder
    .RegisterType<ResourcePool>()
    .As<IResourcePool>()
    .SingleInstance();

builder
    .Register(c => c.Resolve<IResourcePool>().GetResource())
    .As<IResource>()
    .InstancePerLifetimeScope();

Dzięki rejestracji jako InstancePerLifetimeScope, otrzymamy zawsze ten sam obiekt, dla wszystkich zależności w ramach jednego “scope’a”, oraz na zakończenie scope’a zostanie wywołany Dispose i obiekt wróci do puli. Dlatego w miejscu obsługi wywołań asynchronicznych aby uzyskać oddzielny obiekt z puli wystarczy utworzyć nowy LifetimeScope i wywołać handler’a:

using (var scope = lifetimeScope.BeginLifetimeScope())
{
    IRequestHandler requestHandler = scope.Resolve<IRequestHandler>();
    requestHandler.Handle();
}

W moim przypadku, pojawiła się dodatkowo potrzeba opóźnienia pobierania IResource z puli (nie każde wywołanie potrzebowało go do działania), dlatego zaimplementowałem dodatkową klasę LazyResource. Jest to adapter mający na celu opóźnienie wyciągania obiektu do czasu użycia, zamiast w czasie rozwiązywania zależności przy uruchomieniu wątku:

internal class LazyResource : IResource, IDisposable
{
    private readonly IResourcePool resourcePool;
    private IResource resource;

    public LazyResource(IResourcePool resourcePool)
    {
        this.resourcePool = resourcePool;
    }

    private IResource Resource
    {
        get { return resource ?? (resource = resourcePool.GetResource()); }
    }

    public void Method()
    {
        return Resource.Method();
    }

    public void Dispose()
    {
        var disposable = Resource as IDisposable;
        if (disposable != null)
            disposable.Dispose();
    }
}

Jeszcze tylko zmiana rejestracji w kontenerze:

builder
    //.Register(c => c.Resolve<IResourcePool>().GetResource())
    .RegisterType<LazyResource>()
    .As<IResource>()
    .InstancePerLifetimeScope();

Gotowe!

Jedyne co musi zrobić programista aby skorzystać z puli obiektów to dodać parametr konstruktora IResource.

Comments:0

Leave a Reply

Your email address will not be published. Required fields are marked *