Imagine sua aplicação WPF de cotação de preço de serviços que se comunica com um servidor de aplicações usando WCF, e que tenha que carregar todos os dias uma lista de preços atualizada de um sistema legado antigo e lento. Todos os usuários geralmente abrem a aplicação no começo do expediente e a aplicação só pode começar a rodar após a atualização da lista de preços. A lista de preços é enorme e o legado demora por volta de 10 minutos para processar a lista e transmiti-la para a aplicação cliente.
Nesse cenário, a primeira otimização que deve ser feita é cachear essa lista de preços no servidor de aplicações. Idealmente esse processo de cache deve ser feito antes do expediente dos funcionários começar, logo após a atualização da lista de preços no legado. Mas devido a restrições impostas pela maneira que o legado é acessado (no exemplo em questão), esse pré-cache não pode ser feito e ele só pode ocorrer quando o primeiro usuário do sistema abre a aplicação.
Uma solução fácil para cachear essa informação é usar a infraestrutura de caching do Spring Framework. No servidor de aplicações, o serviço que acessa o legado para obter a lista de preços é gerenciada pelo Spring. Basta usar o proxy AOP com um Cache Advice para habilitar o cache (veja mais informações sobre o Cache do Spring aqui).
Porém, a implementação do cache do Spring não é sincronizada. No cenário descrito acima, significa que enquanto a primeira chamada ao legado não completar, outras mais serão executadas no legado pois o cache do Spring diz que o resultado da chamada não esta cacheado e prossegue com a execução do serviço para obter o valor.
Isso significa que se 100 usuário abrirem a aplicação em 10 minutos, que é o tempo que dura a execução de 1 chamada ao legado, serão feitas 100 chamadas ao legado, que ficará sobrecarregado e irá demorar mais de 10 minutos para executar 1 chamada. Para resolver esse problema, basta sincronizar o acesso ao cache, fazendo que apenas 1 chamada ao legado seja feita por dia.
O cache do Spring funciona como um interceptador de chamadas (Method Interceptor). Antes de executar o método, o interceptador verifica se o valor da chamada já esta armazenado no cache. Se está, o interceptador retorna esse valor e nem executa o método interceptado. Senão, prossegue com a chamada e quando o método termina sua execução, o interceptador armazena o valor retornado.
Nossa solução para o cache sincronizado consiste basicamente em um Decorator que expande o comportamento de um cache existente do Spring adicionando o sincronismo que precisamos. Quando o valor do método não esta cacheado, o decorator cria um objeto de sincronismo (implementado com ManualResetEvent) para que chamadas subsequentes para o mesmo método espere a execução da primeira chamada terminar. Quando a primeira chamada termina, o objeto de sincronismo é notificado e descartado. As outras chamadas que estava esperando são liberadas e reprocessadas.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using AopAlliance.Intercept;
using Spring.Aspects.Cache;
using Spring.Caching;
namespace SpringTutorial.Util
{
public class ServerCache : BaseCacheAdvice, ICache
{
private ICache decorated;
private Dictionary<object, ManualResetEvent> locks = new Dictionary<object, ManualResetEvent>();
public int Timeout { get; set; }
public ServerCache(ICache decorated)
{
this.decorated = decorated;
//Default Timeout 20 minutes
Timeout = 20 * 60 * 1000;
}
public void Clear()
{
decorated.Clear();
}
public int Count
{
get { return decorated.Count; }
}
public object Get(object key)
{
object value = decorated.Get(key);
if (value != null)
{
return value;
}
//Not cached
ManualResetEvent keyLock = null;
lock (locks)
{
if (!locks.TryGetValue(key, out keyLock))
{
locks.Add(key, new ManualResetEvent(false));
keyLock = null;
}
}
if (keyLock != null)
{
keyLock.WaitOne(Timeout);
return Get(key);
}
else
{
return decorated.Get(key);
}
}
public void Insert(object key, object value, TimeSpan timeToLive)
{
Insert(key, value);
}
public void Insert(object key, object value)
{
decorated.Insert(key, value);
lock (locks)
{
ManualResetEvent keyLock = null;
if (locks.TryGetValue(key, out keyLock))
{
locks.Remove(key);
keyLock.Set();
}
}
}
public ICollection Keys
{
get { return decorated.Keys; }
}
public void Remove(object key)
{
decorated.Remove(key);
}
public void RemoveAll(ICollection keys)
{
decorated.RemoveAll(new ArrayList(keys));
}
private void UnlockKey(object key)
{
lock (locks)
{
ManualResetEvent keyLock = null;
if (locks.TryGetValue(key, out keyLock))
{
keyLock.Set();
locks.Remove(key);
}
}
}
}
}
Complementando o Decorator acima, é necessário introduzir um comportamento adicional para o caso do método chamado lançar uma exceção. Sem esse complemento, o cache pode ficar travado caso o acesso ao legado dê algum erro. Esse complemento é outro interceptador de chamada.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using AopAlliance.Intercept;
using Spring.Aspects.Cache;
using Spring.Caching;
namespace SpringTutorial.Util
{
public class ServerCache : BaseCacheAdvice, ICache, IMethodInterceptor
{
//(...)
#region IMethodInterceptor Members
public object Invoke(IMethodInvocation invocation)
{
CacheResultAttribute resultInfo = (CacheResultAttribute)GetCustomAttribute(invocation.Method, typeof(CacheResultAttribute));
if (resultInfo != null)
{
IDictionary vars = PrepareVariables(invocation.Method, invocation.Arguments);
object resultKey = resultInfo.KeyExpression.GetValue(null, vars);
try
{
return invocation.Proceed();
}
catch (Exception)
{
//Unlock key so other thread could try to retrieve the values
UnlockKey(resultKey);
throw;
}
}
else
{
return invocation.Proceed();
}
}
#endregion
}
}
A configuração do Spring:
<?xml version="1.0" encoding="utf-8" ?>
<objects xmlns="http://www.springframework.net" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.net http://www.springframework.net/xsd/spring-objects.xsd">
<object id="cacheAspect" type="Spring.Aspects.Cache.CacheAspect, Spring.Aop">
</object>
<object id="springCache" type="Spring.Caching.NonExpiringCache, Spring.Core">
</object>
<object id="serverCache" type="SpringTutorial.Util.ServerCache, SpringTutorial.Util">
<constructor-arg ref="springCache" />
</object>
<object id="pricebookServiceTarget" type="SpringTutorial.Core.PricebookService, SpringTutorial.Core">
</object>
<object id="pricebookService" type="Spring.Aop.Framework.ProxyFactoryObject, Spring.Aop">
<property name="proxyInterfaces" value="SpringTutorial.IPricebookService"/>
<property name="target" ref="pricebookServiceTarget"/>
<property name="interceptorNames">
<list>
<value>serverCache</value>
<value>cacheAspect</value>
</list>
</property>
</object>
</objects>
Dado as restrições que temos na arquitetura atual do sistema, esse é o método menos intrusivo para resolver esse problema de performance no acesso ao legado.
Referencias:
