Soporte de multihilo en clientes de correo

Clientes de correo como ImapClient y Pop3Client permitir a los usuarios utilizarlos en un entorno de multihilo. Los clientes de correo permiten tener una o más conexiones con un servidor. Para gestionar un conjunto de conexiones dentro de los clientes se utiliza un pool de conexiones. La cantidad de conexiones que pueden crearse y usarse simultáneamente está limitada por la propiedad CredentialsByHostClient.MaxConnectionsPerServer. Esta propiedad puede establecerse en 1 o en un valor mayor. Por defecto, esta propiedad es 10. Se ha implementado una cola de comandos para la conexión para soportar operaciones multihilo. Los comandos implementan las operaciones más simples definidas en el protocolo, como Noop, Authenticate, etc. El usuario puede iniciar la ejecución de una mayor cantidad de comandos que conexiones disponibles. Sin embargo, se ejecutarán solo cuando el cliente pueda crear una conexión para la operación.

Métodos de uso de clientes de correo en entorno de multihilo

Los clientes de correo tienen el siguiente comportamiento:

1. En caso de que MaxConnectionsPerServer = 1, el cliente crea 1 conexión, realiza la autenticación y autorización. Esta conexión se mantiene en estado activo hasta que el cliente sea eliminado. Todas las operaciones de diferentes hilos se dirigen a una única cola de comandos ubicada en la conexión principal.

2. En caso de que MaxConnectionsPerServer > 1, el cliente crea la cantidad necesaria de conexiones, realiza la autenticación y autorización para cada conexión. Una conexión se reserva como conexión principal. Esta conexión se mantiene en estado activo hasta que el cliente sea eliminado. Todas las demás conexiones se crean y eliminan bajo demanda. La cantidad máxima de dichas conexiones está definida por la propiedad MaxConnectionsPerServer, es decir, por ejemplo, si MaxConnectionsPerServer = 2, una se reserva como conexión principal y la segunda se usa como adicional para operaciones que se ejecutan en otros hilos. De forma similar, si MaxConnectionsPerServer = 3, la primera conexión se reserva como principal y las dos restantes se usan como adicionales para operaciones que se ejecutan en otros hilos. En caso de que llegue una nueva solicitud de conexión desde un hilo nuevo, y todas las conexiones ya estén en uso, el cliente espera hasta que el número de conexiones usadas disminuya. Este es un momento muy importante que aclara por qué la eliminación correcta de conexiones es tan importante.

Ejemplos

El usuario puede ejecutar operaciones en diferentes hilos de varias maneras. Pueden dividirse en dos tipos:

1. El usuario utiliza métodos asíncronos (Begin/End) definidos en el cliente. En este caso el cliente de correo lanza nuevos hilos cuando es necesario. En el cliente se implementa una cola de tareas (por favor, no confundir con la cola de comandos en la conexión). Una tarea puede ejecutarse si hay una conexión disponible. Una vez que la cantidad de conexiones usadas sea menor que el valor límite, el cliente crea una nueva conexión, crea un hilo para la tarea actual y ejecuta dicha tarea. Ejemplo de uso de operaciones asíncronas:

// Create an imapclient with host, user and password
ImapClient client = new ImapClient();
client.setHost("domain.com");
client.setUsername("username");
client.setPassword("password");
client.selectFolder("InBox");

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. El usuario puede crear hilos usando objetos como Thread, ExecutorService o cualquier otro objeto destinado a este propósito. También el usuario puede usar hilos creados en código de terceros. Durante esto el cliente tiene dos modelos de comportamiento

a. Si el usuario no ha creado conexiones adicionales para operaciones en el hilo, todas las operaciones de este hilo se enviarán a la cola de comandos de la conexión principal. Un ejemplo de operaciones en un hilo adicional sin crear una nueva conexión, todas las transacciones se realizan a través de la conexión principal:

/**
 * Creates an executor service with a fixed pool size, that will time 
 * out after a certain period of inactivity.
 * 
 * @param poolSize The core- and maximum pool size
 * @param keepAliveTime The keep alive time
 * @param timeUnit The time unit
 * @return The executor service
 */
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("folderName");
        ImapMessageInfoCollection messageInfoCol = client.listMessages();
        for (ImapMessageInfo messageInfo : messageInfoCol) {
            list.add(client.fetchMessage(messageInfo.getUniqueId()));
        }
    }
});

b. Cuando el usuario ejecuta el método para crear una nueva conexión para un hilo adicional, este hilo se bloquea hasta que el valor de cuota para nuevas conexiones sea cambiado para permitir la nueva conexión. Luego se crea la nueva conexión. Esta conexión se establece como la conexión predeterminada para todas las operaciones en este hilo. Después de que se completen todas las operaciones en este hilo, la conexión debe ser eliminada. Para crear nuevas conexiones use el método CredentialsByHostClient.CreateConnection. Este método devuelve un objeto que implementa la interfaz IDisposable. Para liberar la conexión se debe invocar el método Dispose. La creación y eliminación de la conexión deben ejecutarse dentro del hilo donde se ejecutan las operaciones de correo. Intentar crear una nueva conexión en el hilo donde se creó el cliente de correo genera un error, porque ese hilo en ese momento no puede usarse para crear una nueva conexión. Además, la creación de una nueva conexión no es posible cuando MaxConnectionsPerServer = 1. Un ejemplo de código de creación de una nueva conexión en un hilo 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, "FolderName");
            ImapMessageInfoCollection messageInfoCol = client.listMessages(connection);
            for (ImapMessageInfo messageInfo : messageInfoCol)
                list.add(client.fetchMessage(connection, messageInfo.getUniqueId()));
        } finally {
            connection.dispose();
        }
    }
});

Recomendaciones

Vale la pena señalar que si el usuario envía todos los comandos a la conexión principal, puede surgir una situación en la que los comandos de diferentes hilos se mezclen. El usuario debe comprender qué comandos dependen de su secuencia y tomar medidas para la sincronización de dichos comandos. También es necesario considerar la posibilidad de ejecutar comandos en diferentes sesiones (IMAP/POP3). Las operaciones que consumen más tiempo, como FetchMessage, AppendMessage y Send, probablemente tengan sentido de realizarse con un nuevo hilo y una nueva conexión. Pero operaciones rápidas como Delete tienen sentido de ejecutarse con la conexión principal. Tenga en cuenta que la inicialización de una nueva conexión es una operación suficientemente costosa en tiempo.