Підтримка багатопотоковості в поштових клієнтах

Поштові клієнти, такі як ImapClient і Pop3Client дозволяють користувачам використовувати їх у багатопотоковому середовищі. Поштові клієнти дозволяють мати одне або кілька з’єднань з сервером. Для керування набором з’єднань всередині клієнтів використовується пул з’єднань. Кількість з’єднань, які можуть бути створені та використані одночасно, обмежується властивістю CredentialsByHostClient.MaxConnectionsPerServer. Ця властивість може бути встановлена в 1 або більше. За замовчуванням вона дорівнює 10. Для з’єднання реалізована черга команд для підтримки багатопотокових операцій. Команди реалізують найпростіші операції, визначені протоколом, такі як Noop, Authenticate тощо. Користувач може запустити виконання більшої кількості команд, ніж доступна кількість з’єднань. Однак вони будуть виконані лише тоді, коли клієнт зможе створити з’єднання для операції.

Методи використання поштових клієнтів у багатопотоковому середовищі

Поштові клієнти мають таку поведінку:

1. Якщо MaxConnectionsPerServer = 1, клієнт створює 1 з’єднання, виконує автентифікацію та авторизацію. Це з’єднання підтримується у робочому стані до моменту, коли клієнт буде звільнений. Усі операції з різних потоків направляються в одну чергу команд, розміщену в головному з’єднанні.

2. Якщо MaxConnectionsPerServer > 1, клієнт створює необхідну кількість з’єднань, виконує автентифікацію та авторизацію для кожного з’єднання. Одне з’єднання зарезервовано як головне з’єднання. Це з’єднання підтримується у робочому стані до моменту, коли клієнт буде звільнений. Інші з’єднання створюються та звільняються за потребою. Максимальна кількість таких з’єднань визначається властивістю MaxConnectionsPerServer, наприклад, якщо MaxConnectionsPerServer = 2, то одне зарезервовано як головне з’єднання, а друге використовується як додаткове для операцій, які виконуються в інших потоках. Аналогічно, якщо MaxConnectionsPerServer = 3, то перше з’єднання зарезервовано як головне, а два інші використовуються як додаткові для операцій в інших потоках. У випадку надходження запиту на з’єднання від нового потоку, і коли всі з’єднання вже зайняті, клієнт чекає, доки кількість використаних з’єднань не зменшиться. Це дуже важливий момент, який пояснює, чому правильне звільнення з’єднань є таким важливим.

Приклади

Користувач може виконувати операції в різних потоках кількома способами. Їх можна розділити на два типи:

1. Користувач використовує асинхронні (Begin/End) методи, визначені в клієнті. У цьому випадку поштовий клієнт запускає нові потоки за потреби. У клієнті реалізована черга задач (будь ласка, не плутайте її з чергою команд у з’єднанні). Задача може бути виконана, якщо з’єднання доступне. Коли кількість використаних з’єднань стає меншою за межеве значення, клієнт створює нове з’єднання, створює потік для поточної задачі і виконує її. Приклад використання асинхронних операцій:

// 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. Користувач може створювати потоки, використовуючи такі об’єкти як Thread, ExecutorService або інші об’єкти, передбачені для цієї мети. Також користувач може використовувати потоки, створені у сторонньому коді. При цьому клієнт має дві моделі поведінки

a. Якщо користувач не створює додаткових з’єднань для операцій у потоці, всі операції для цього потоку будуть відправлені в чергу команд головного з’єднання. Приклад операцій у додатковому потоці без створення нового з’єднання, всі транзакції виконуються через головне з’єднання:

/**
 * 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. Коли користувач виконує метод створення нового з’єднання для додаткового потоку, цей потік блокується, поки значення квоти для нових з’єднань не буде змінено, щоб дозволити нове з’єднання. Потім нове з’єднання створюється. Це з’єднання встановлюється як дефолтне з’єднання для всіх операцій у цьому потоці. Після завершення всіх операцій у цьому потоці з’єднання має бути звільнене. Для створення нових з’єднань використовуйте метод CredentialsByHostClient.CreateConnection. Цей метод повертає об’єкт, який реалізує інтерфейс IDisposable. Для звільнення з’єднання треба викликати метод Dispose. Створення та звільнення з’єднання повинні виконуватись всередині потоку, у якому виконуються поштові операції. Спроба створити нове з’єднання у потоці, в якому був створений поштовий клієнт, призводить до помилки, оскільки цей потік у даний момент не може бути використаний для створення нового з’єднання. Також створення нового з’єднання неможливе, коли MaxConnectionsPerServer = 1. Приклад коду створення нового з’єднання в додатковому потоці:

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();
        }
    }
});

Рекомендації

Варто зазначити, що якщо користувач надсилає всі команди через головне з’єднання, може виникнути ситуація, коли команди з різних потоків будуть перемішані. Користувач повинен розуміти, які команди залежать від їх послідовності, і вжити заходів для синхронізації таких команд. Також необхідно враховувати можливість виконання команд у різних сеансах (IMAP/POP3). Найбільш ресурсоємними операціями є, наприклад, FetchMessage, AppendMessage та Send. Ймовірно, має сенс виконувати ці операції у новому потоці і новому з’єднанні. Але швидкі операції, такі як Delete, доцільно виконувати через головне з’єднання. Зверніть увагу, що ініціалізація нового з’єднання є достатньо витратною операцією.