Suporte a multi-threading em clientes de email
Clientes de email como ImapClient e Pop3Client permitem que os usuários os utilizem em um ambiente de multi-threading. Clientes de email permitem ter uma ou mais conexões com um servidor. Para gerenciar um conjunto de conexões dentro dos clientes, é utilizado um pool de conexões. A quantidade de conexões que podem ser criadas e usadas ao mesmo tempo é limitada pela propriedade CredentialsByHostClient.MaxConnectionsPerServer. Essa propriedade pode ser definida como 1 ou um valor maior. Por padrão, essa propriedade é igual a 10. A fila de comandos foi implementada para a conexão a fim de suportar operações de multi-threading. Os comandos implementam as operações mais simples definidas no protocolo, como Noop, Authenticate e assim por diante. O usuário pode iniciar a execução de uma quantidade maior de comandos do que a quantidade de conexões disponíveis. Porém, serão executados apenas quando o cliente puder criar uma conexão para a operação.
Métodos de Uso de Clientes de Email em Ambiente de Multi-Threading
Clientes de email têm o seguinte comportamento:
1. No caso de MaxConnectionsPerServer = 1, o cliente cria 1 conexão, realiza autenticação e autorização. Essa conexão é mantida em estado ativo até o momento em que o cliente for descartado. Todas as operações de diferentes threads são direcionadas a uma fila de comandos colocada na conexão principal.
2. No caso de MaxConnectionsPerServer > 1, o cliente cria a quantidade necessária de conexões, realiza autenticação e autorização para cada conexão. Uma conexão é reservada como conexão principal. Essa conexão é mantida em estado ativo até o momento em que o cliente for descartado. Todas as outras conexões são criadas e descartadas conforme a demanda. A quantidade máxima de tais conexões é definida pela propriedade MaxConnectionsPerServer, ou seja, por exemplo, se MaxConnectionsPerServer = 2, então uma é reservada como conexão principal, e a segunda conexão é usada como adicional para operações que são executadas em outras threads. Assim, se MaxConnectionsPerServer = 3, a primeira conexão é reservada como conexão principal, e duas outras conexões são usadas como adicionais para operações que são executadas em outras threads. No caso de uma nova solicitação de conexão de uma nova thread, e todas as conexões já estiverem em uso, o cliente aguarda até que o número de conexões usadas seja reduzido. Este é um momento muito importante que esclarece por que o descarte correto das conexões é tão importante.
Exemplos
O usuário pode executar operações em diferentes threads de várias maneiras. Elas podem ser divididas em dois tipos:
1. O usuário usa métodos assíncronos (Begin/End) definidos no cliente. Nesse caso, o cliente de email inicia novas threads quando necessário. No cliente, é implementada uma fila de tarefas (por favor, não confunda com a fila de comandos na conexão). A tarefa pode ser executada se a conexão estiver disponível. Assim que a quantidade de conexões usadas se torna menor que o valor limite, o cliente cria uma nova conexão, cria uma thread para a tarefa atual e executa essa tarefa. Exemplo de uso de operações assíncronas:
// Cria um imapclient com host, usuário e senha
ImapClient client = new ImapClient();
client.setHost("dominio.com");
client.setUsername("usuario");
client.setPassword("senha");
client.selectFolder("Caixa de Entrada");
ImapMessageInfoCollection messages = client.listMessages();
IAsyncResult res1 = client.beginFetchMessage(messages.get_Item(0).getUniqueId());
IAsyncResult res2 = client.beginFetchMessage(messages.get_Item(1).getUniqueId());
MailMessage msg1 = client.endFetchMessage(res1);
MailMessage msg2 = client.endFetchMessage(res2);
2. O usuário pode criar threads usando objetos como Thread, ExecutorService ou quaisquer outros objetos destinados a esse propósito. Além disso, o usuário pode usar threads criadas em código de terceiros. Durante isso, o cliente possui dois modelos de comportamento
a. Se o usuário não tiver iniciado a criação de conexões adicionais para operações na thread, todas as operações para esta thread serão enviadas para a fila de comandos da conexão principal. Um exemplo de operações em uma thread adicional sem criar uma nova conexão, todas as transações são realizadas via a conexão principal:
/**
* Cria um serviço executor com um tamanho de pool fixo, que irá expirar
* após um certo período de inatividade.
*
* @param poolSize O tamanho do pool principal e máximo
* @param keepAliveTime O tempo de manutenção
* @param timeUnit A unidade de tempo
* @return O serviço executor
*/
private static ExecutorService createFixedTimeoutExecutorService(
int poolSize, long keepAliveTime, TimeUnit timeUnit)
{
ThreadPoolExecutor e =
new ThreadPoolExecutor(poolSize, poolSize,
keepAliveTime, timeUnit, new LinkedBlockingQueue<Runnable>());
e.allowCoreThreadTimeOut(true);
return e;
}
final List<MailMessage> list = new ArrayList<MailMessage>();
ExecutorService executor = createFixedTimeoutExecutorService(1, 1000, TimeUnit.MILLISECONDS);
executor.execute(new Runnable() {
public void run() {
client.selectFolder("nomeDaPasta");
ImapMessageInfoCollection messageInfoCol = client.listMessages();
for (ImapMessageInfo messageInfo : messageInfoCol) {
list.add(client.fetchMessage(messageInfo.getUniqueId()));
}
}
});
b. Quando o usuário executa o método para criar uma nova conexão para a thread adicional, essa thread é bloqueada até que o valor da cota para novas conexões seja alterado para permitir uma nova conexão. Então uma nova conexão é criada. Essa conexão é definida como a conexão padrão para todas as operações nesta thread. Após todas as operações nesta thread serem concluídas, a conexão deve ser descartada. Para criar novas conexões, use o método CredentialsByHostClient.CreateConnection. Esse método retorna um objeto que implementa a interface IDisposable. Para liberar a conexão, o método Dispose deve ser invocado. A criação e o descarte da conexão devem ser executados dentro da thread onde as operações de email são realizadas. Tentar criar uma nova conexão na thread onde o cliente de email foi criado leva a um erro, porque esta thread, neste momento, não pode ser usada para a criação de uma nova conexão. Além disso, a criação de nova conexão não é possível quando MaxConnectionsPerServer = 1. Um exemplo de código para criar uma nova conexão em uma thread adicional:
final List<MailMessage> list = new ArrayList<MailMessage>();
ExecutorService executor = createFixedTimeoutExecutorService(1, 1000, TimeUnit.MILLISECONDS);
executor.execute(new Runnable() {
public void run() {
IConnection connection = client.createConnection();
try {
client.selectFolder(connection, "NomeDaPasta");
ImapMessageInfoCollection messageInfoCol = client.listMessages(connection);
for (ImapMessageInfo messageInfo : messageInfoCol)
list.add(client.fetchMessage(connection, messageInfo.getUniqueId()));
} finally {
connection.dispose();
}
}
});
Recomendações
Vale ressaltar que se o usuário enviar todos os comandos para a conexão principal, pode surgir uma situação em que comandos de diferentes threads sejam misturados. O usuário deve entender quais comandos são dependentes de sua sequência e tomar medidas para a sincronização desses comandos. Também é necessário considerar a possibilidade de executar comandos em diferentes sessões (IMAP/POP3). As operações mais demoradas, como FetchMessage, AppendMessage e Send, provavelmente fazem sentido serem executadas com uma nova thread e uma nova conexão. Mas operações rápidas como Delete fazem sentido serem realizadas com a conexão principal. Por favor, note que a inicialização de uma nova conexão é uma operação suficientemente demorada.