Асинхронный клиент.

Версия 1.0.1

© Copyright 2006
Грибов Игорь,
e-mail: gribov@depni.sinp.msu.ru

Оглавление.

Изменения в версиях.
Задачи клиента.
Программное решение.

Изменения в версиях.

Полная версия раздела задается в виде: версия.подверсия.выпуск.

Номер версии увеличивается при появлении глав, затрагивающих не рассмотренные ранее вопросы. Названия этих глав выносятся в оглавление раздела. Подверсия увеличивается, когда в существующие главы добавляются новые абзацы, рисунки, либо значительно перерабатывается имеющийся материал, существенно изменяя содержание главы. А при внесении лишь незначительных правок в существующие главы увеличивается номер выпуска.


Задачи клиента.

Главной задачей клиента является обеспечение приложений данными, которые запрашиваются у одного или нескольких серверов. Для ее решения клиент должен уметь выполнять транзакции, включающие в себя несколько этапов (см. рисунок). Получив необходимые данные от приложения, клиент формирует и отправляет запросы серверам. Для каждого успешно выполненого запроса клиент ожидает ответ сервера, контролируя истечение максимального времени его получения (тайм-аут). Выдача результата производится для всех запрошенных приложением объектов (параметров), в том числе тех, которые не удалось отправить серверу, либо на которые не был получен ответ. Соответственно, каждый выдаваемый результат содержит статус завершения транзакции.

Помимо традиционных сервисов к асинхронному – в смысле сигналобезопасности – клиенту предъявляется дополнительное требование. Он должен предоставлять возможность повторно-входимого вызова. То есть с его помощью в принципе можно выполнить одну клиентскую транзакцию внутри другой. Конечно, такая возможность предполагает некоторые ограничения и должна использоваться с осторожностью. При асинхронном запросе данных у от одного и того же сервера следует убедиться, что он также в состоянии отрабатывать вложенные запросы. Далеко не все серверы поддерживают такой режим работы, даже если запрашиваются различные объекты. А вот асинхронные клиентские запросы к различным серверам как правило вполне допустимы. При этом, однако, может потребоваться увеличение времени тайм-аута отдельных этапов транзакции.



Программное решение.

#define SIZE_CLTWORK  16

Параметр SIZE_CLTWORK задает размер буфера клиентских транзакций. Каждому объекту транзакции (запросу клиента) выделятся один элемент буфера. Поэтому значение SIZE_CLTWORK определяет максимальное число как одновременно проходящих однопараметрических транзакций, так и число запрашиваемых параметров в рамках одной многопараметрической транзакции.

#define ERROR  -1
#define OK     1

#define STATE_WAIT_REPLY        1    // Ожидание ответа сервера
#define STATE_OK                0    // Нормальное завершение транзакции
#define STATE_RETRIEVE_TIMEOUT  -1   // Тайм-аут ответа сервера
#define STATE_READ_TIMEOUT      -2   // Тайм-аут чтения полученного ответа
#define STATE_TRANS_TIMEOUT     -3   // Тайм-аут цикла ожидания транзакции
#define STATE_NOBUFFER          -4   // Нет свободного элемента в буфере транзакций
#define STATE_WRITERR           -5   // Ошибка отправки запроса серверу
#define STATE_UNABLE            -6   // Невозможно выполнить транзакцию

#define TIMER_PERIOD_MCS  10000      // Период таймера (мкс)

#define TIMEOUT_RETRIEVE  1000000    // Тайм-аут получения ответа сервера (мкс)
#define TIMEOUT_READ      4000000    // Тайм-аут чтения полученных данных (мкс)
#define SLEEP_READ        50000      // Период цикла ожидания транзакции (мкс)

#define PRIORITY_3        3          // Приоритет клиентского запроса данных

typedef char            int8;
typedef unsigned char   unsigned8;
typedef short           int16;
typedef unsigned short  unsigned16;
typedef int             int32;
typedef unsigned int    unsigned32;

typedef struct {
   unsigned32 key;     // Уникальный ключ (идентификатор) объекта транзакции
   unsigned8 data_1;
   unsigned16 data_2;
   int32 data_3;
} data;                // Структура объекта транзакции

Помимо собственно данных data_1, data_2, data_3 структура содержит поле ключа key, значение которого должно быть уникально для любых запрашиваемых клиентом объектов. По значению ключа клиент определяет, на какой именно запрос получен ответ сервера. Ключом может являться некоторая совокупность параметров, формирующая уникальный идентификатор объекта.

typedef struct {
   int16 state;   // Статус транзакции
   data dt;
} clientappl;     // Структура приложения клиента

Структура приложения клиента содержит дополнительное поле статуса транзакции. Оно используется клиентом для переключения стадий транзакции, а после ее завершения содержит код результата.

typedef struct {
   int16 busy;           // Семафор занятости буфера
   unsigned16 timeout;   // Счетчик тайм-аута
   clientappl ca;
} clientwork;            // Структура буфера транзакции

Структура буфера транзакции клиента дополняет структуру приложения clientappl семафором занятости элемента буфера busy и счетчиком тайм-аута транзакции timeout.

clientwork cltwork[SIZE_CLTWORK];   // Буфер транзакций клиента
int16 sem_reset;                    // Семафор контроля тайм-аута
int16 flag_reset;                   // Cчетчик просчетов тайм-аута
int32 sleep_cnt;                    // Счетчик времени задержки

void micro_sleep(unsigned32 microseconds)   // ms_0
{
   sleep_cnt = (microseconds / TIMER_PERIOD_MCS) + 1;
   while (sleep_cnt > 0);
}

Функция micro_sleep(microseconds) обеспечивает временную задержку с точностью до периода вызова нижеприведенной функции periodic(). Время задержки задается в микросекундах. Такая реализация функции задержки уместна в приложениях без использования операционной системы. Если же ОС поддерживает функции “засыпания”: sleep(...), nanosleep(...) и т.п., целесообразно пользоваться именно этими функциями.

void resetwork(clientwork *cw)   // rw_0
{
   cw->ca.state = STATE_OK;
   cw->timeout = TIMEOUT_RETRIEVE / TIMER_PERIOD_MCS;   // rw_3
   cw->busy = -1;                                       // rw_4
}

Функция resetwork(cw) осуществляет инициализацию (сброс) элемента буфера транзакции клиента. При этом величина тайм-аута подготавливается для новой транзакции – устанавливается время ожидания ответа сервера [rw_3]. А семафор занятости элемента буфера принудительно открывается [rw_4]. Структура объекта транзакции не очищается, поскольку за её содержимое отвечает приложение клиента. Для обеспечения состоятельности буфера транзакции его инициализация не должна прерываться вызовом тайм-аут контроля client_control(). Так, если для занятого буфера – с установленным флагом busyтайм-аут контроль будет осуществлён после выполнения оператора [rw_3], но до [rw_4] значение счетчика тайм-аута уменьшится на единицу.

void client_control(void)   // cc_0
{
   unsigned16 cnt;

   sem_reset++;            // cc_4
   if (sem_reset != 0) {   // cc_5
      flag_reset++;        // cc_6
      sem_reset--;         // cc_7
      return;
   }
   for (cnt = 0; cnt < SIZE_CLTWORK; cnt++) {         // cc_10
      if (cltwork[cnt].busy < 0) continue;            // cc_11
      if (cltwork[cnt].timeout > 0) {                 // cc_12
         cltwork[cnt].timeout--;                      // cc_13
      } else {                                        // cc_14
         if (cltwork[cnt].ca.state == STATE_WAIT_REPLY) {             // cc_15
            cltwork[cnt].ca.state = STATE_RETRIEVE_TIMEOUT;           // cc_16
            cltwork[cnt].timeout = TIMEOUT_READ / TIMER_PERIOD_MCS;   // cc_17
         } else {                                                     // cc_18
            resetwork(cltwork + cnt);                                 // cc_19
         }
      }
   }
   sem_reset--;            // cc_23
}

void control_lock(void)   // cl_0
{
   sem_reset++;
}

void control_unlock(void)   // cu_0
{
   sem_reset--;                     // cu_2
   while (flag_reset > 0) {         // cu_3
      if (sem_reset != -1) break;   // cu_4
      client_control();             // cu_5
      flag_reset--;                 // cu_6
   }
}

Три функции: client_control(), control_lock() и control_unlock() обеспечивают контроль тайм-аутов клиентских транзакций с возможностью досчета пропущенных циклов контроля. В функции client_control() для каждой активной транзакции - с установленным флагом busy [сc_11] - контролируется счетчик тайм-аута [сc_12], [сc_13]. В случае наступления события тайм-аута [сc_14] выполняемые действия зависят от стадии транзакции. Если клиент ожидал ответа сервера [сc_15], транзакция будет завершена со статусом тайм-аута ответа [сc_16]. Если же ответ сервера был получен, но данные не считаны приложением за отведенное время, транзакция сбрасывается [сc_19], что приведет к установу статуса тайм-аута чтения полученного ответа. Обратите внимание, что при обработке условия [сc_15] устанавливается новое значение счетчика, соответствующее тайм-ауту чтения полученного ответа [сc_17]. Поэтому если приложение не сумеет вовремя считать результат транзакции, информация о тайм-ауте ответа сервера будет утеряна. Такой подход, однако, гарантирует, что по истечении всех тайм-аутов, в том числе цикла ожидания транзакции, ее буфер будет сброшен и станет доступен для использования новыми транзакциями.

Функция client_control() выполнена в сигналобезопасном варианте (защищена семафором sem_reset [сc_4], [сc_23]). Однако, ее асинхронный вызов не предусматривается – она активируется из функции таймера, которая предотвращает повторную входимость собственными силами. Назначение этого семафора – блокировать работу функции client_control() в моменты времени, когда ее асинхронный – по отношению к самой клиентской транзакции – вызов способен привести к нежелательным результатам. А для того, чтобы во время таких блокировок, которые могут оказаться довольно частыми и длительными, избежать просчета циклов контроля, служит счетчик flag_reset. Он ведет подсчет [сc_6] пропущенных вследствие блокирования [сc_5] циклов контроля тайм-аута.

Для запрета тайм-аут контроля служит функция control_lock(). Ее содержание минимально – только закрытие семафора – и она определена исключительно для программной симметрии вызовов блокирования и деблокирования. Досчет пропущенных циклов производится именно при разрешении тайм-аут контроля функцией control_unlock(). Цикл досчета [сu_3] будет работать только при открытом состоянии семафора контроля, то есть когда функция контроля тайм-аута вновь становится активной. При обнаружении блокированного состояния семафора [сu_4] цикл [сu_3] обрывается, поскольку в этом случае мы получили бы зацикливание процедуры досчета. Причем контроль sem_reset осуществляется именно внутри цикла [сu_3] на случай, если будет осуществлен асинхронный вызов функции блокирования тайм-аут контроля control_lock().

Отметим особенности работы функции control_unlock(), связанные с ее возможным повторном вызовом при асинхронном управлении блокировками тайм-аут контроля. Если такой вызов произойдет внутри цикла [сu_3] до оператора [сu_5], по возвращении из него будет произведено дополнительное лишнее обращение к client_control(), а счетчик flag_reset получит отрицательное значение (именно для таких случаев он определен со знаком). Таким образом, для одних параметров будет произведен дополнительный цикл контроля тайм-аута, в то время как другие могут в дальнейшем не досчитаться одного цикла контроля. При повторном вызове control_unlock() после [сu_5], но до [сu_6] мы получим лишь недосчет (в будущем) одного цикла контроля, поскольку значение flag_reset станет отрицательным. Однако, поскольку параметры тайм-аута всегда выбираются с запасом, одиночный недосчет либо пересчет цикла контроля не следует считать значимым.

int16 write_data(data *dt, unsigned16 priority)
{
   return OK ;
   return ERROR;
}

Функция записи данных write_data(dt, priority) направляет запросы клиента серверу, организуя асинхронный приоритетный доступ к системе вывода данных. Вариант ее реализации приведен в главе “Процесс пошел”.

void server_reply(data *dt)   // sr_0
{
   unsigned16 cnt;

   control_lock();                                                 // sr_4
   for (cnt = 0; cnt < SIZE_CLTWORK; cnt++) {                      // sr_5
      if (cltwork[cnt].ca.state != STATE_WAIT_REPLY) continue;     // sr_6
      if (cltwork[cnt].ca.dt.key == dt->key) {                     // sr_7
         cltwork[cnt].ca.dt = *dt;                                 // sr_8
         cltwork[cnt].ca.state = STATE_OK;                         // sr_9
         cltwork[cnt].timeout = TIMEOUT_READ / TIMER_PERIOD_MCS;   // sr_10
         break;                                                    // sr_11
      }
   }
   control_unlock();                                               // sr_14
}

Функция server_reply(dt) принимает ответы сервера. Ее вызов производится как правило асинхронно относительно выполнения других функций приложения клиента – по сигналу приема данных. Алгоритм обработки этого сигнала может быть реализован подобно тому, как описано в разделе “полное чтение данных” главы “Малые замечания”. Для исключения маловероятной, но возможной ситуции наложения приема данных от сервера на возникновениа тайм-аута ожидания этих данных контроль последнего блокируется [sr_4], [sr_14]. Обратная ситуация (прием данных от сервера во время контроля тайм-аута) невозможна, поскольку приоритет сигнала таймера – наивысший. При поступлении ответа производится линейный просмотр буфера транзакций (цикл [sr_5]) с пропуским тех из них, которые не ожидают ответа сервера [sr_6]. Из транзакций, находящихся в состоянии ожидания, выбирается та, уникальный идентификатор которой совпадает со значением, возвращаемым сервером [sr_7]. Для этой транзакции сохраняются полученные данные [sr_8], устанавливается статус нормального завершения [sr_9] и активируется счетчик тайм-аута чтения принятого ответа [sr_10]. Если походящая транзакция не обнаружена (сброшена, например, вследствие тайм-аута), полученные от сервера данные не используются.

void read_server_data(clientappl *ca)   // sd_0
{
   unsigned16 cnt;

   control_lock();                                          // sd_4
   for (cnt = 0; cnt < SIZE_CLTWORK; cnt++) {               // sd_5
      if (cltwork[cnt].busy < 0) continue;                  // sd_6
      if (cltwork[cnt].ca.dt.key == ca->dt.key) {           // sd_7
         if (cltwork[cnt].ca.state != STATE_WAIT_REPLY) {   // sd_8
            *ca = cltwork[cnt].ca;                          // sd_9
            resetwork(cltwork + cnt);                       // sd_10
         }
         control_unlock();                                  // sd_12
         return;                                            // sd_13
      }
   }
   control_unlock();                                        // sd_16
   ca->state = STATE_READ_TIMEOUT;                          // sd_17
}

Функция read_server_data(ca) осуществляет переправку клиентскому приложению принятых от сервера данных. С этой целью она опрашивается монитором клиента в процессе ожидания ответа сервера. Для исключения возможного наложения считывания принятых данных и тайм-аута, контроль последнего блокируется [sd_4], [sd_12], [sd_16]. Так, если тайм-аут чтения полученного ответа сработает после проверки условия [sd_8], но до завершения копирования находящихся в буфере транзакции данных [sd_9], последние могут оказаться несостоятельными вследствие захвата освободившегося буфера новой клиентской транзакцией. Функция просматривает буфер транзакций (цикл [sd_5]) с пропуским свободных элементов [sd_6]. Из активных выбирается транзакция, уникальный идентификатор которой совпадает со значением, определеным приложением клиента [sd_7]. Если статус этой транзакции изменился (стал отличен от изначально установленного ожидания ответа сервера), все данные транзакции, а также ее итоговый статус возвращаются клиенту [sd_9]. После этого освобождается соответствующий буфер [sd_10]. Именно смена статуса укажет монитору клиента на завершение транзакции. А если ее статус не изменился, судьбу транзакции решат последующие вызовы функции [sd_13]. Когда же нужную транзакцию обнаружить не удалось, считается, что она была сброшена в результате тайм-аута чтения полученного ответа (client_control(), [сc_19]) и для клиента устанавливается соответствующий итоговый статус [sd_17].

void request_transaction(clientappl *ca)   // rt_0
{
   unsigned16 cnt;

   control_lock();                                      // rt_4
   for (cnt = 0; cnt < SIZE_CLTWORK; cnt++) {           // rt_5
      cltwork[cnt].busy++;                              // rt_6
      if (cltwork[cnt].busy == 0) {                     // rt_7
         cltwork[cnt].ca = *ca;                         // rt_8
         cltwork[cnt].ca.state = STATE_WAIT_REPLY;      // rt_9
         if (write_data(&ca->dt, PRIORITY_3) == OK) {   // rt_10
            ca->state = STATE_WAIT_REPLY;               // rt_11
         } else {                                       // rt_12
            ca->state = STATE_WRITERR;                  // rt_13
            resetwork(cltwork + cnt);                   // rt_14
         }
         control_unlock();                              // rt_16
         return;
      }
      cltwork[cnt].busy--;                              // rt_19
   }
   control_unlock();                                    // rt_21
   ca->state = STATE_NOBUFFER;                          // rt_22
}

Функция request_transaction(ca) осуществляет запрос данных у сервера и тем самым инициирует транзакцию клиента. Для исключения возможного наложения захвата буфера транзакции и тайм-аута, контроль последнего блокируется [rt_4], [rt_16], [rt_21]. Так, если тайм-аут чтения, сопровождающийся сбросом буфера, сработает вслед за выполнением попытки его захвата [rt_6], значение семафора занятости буфера после выполнения оператора [rt_19] станет меньше минус 1 и он будет постоянно блокирован.

Функция запроса данных сигналобезопасно просматривает буфер транзакций (цикл [rt_5]), и при обнаружении свободного элемента [rt_6], [rt_7] производит его заполнение данными [rt_8] и установ статуса ожидания ответа сервера [rt_9]. Затем осуществляется попытка записи данных и если она выполняется удачно [rt_10], монитору клиента возвращается статус ожидания ответа сервера [rt_11], чем подтверждается успешная инициализация транзакции. При возникновении ошибки записи данных [rt_12] клиенту возвращается ее статус [rt_13], а буфер транзакции сбрасывается [rt_14]. Если не нашлось ни одного свободного буфера транзакции, монитору клиента также возвращается соответствующий статус [rt_22]. Отметим, что функция запроса данных по результатам своего выполнения обязательно должна установить определенный статус транзакции клиента.

void client_transaction(clientappl *ca)   // ct_0
{
   unsigned32 tout;

   request_transaction(ca);                        // ct_4
   if (ca->state != STATE_WAIT_REPLY) return;      // ct_5
   tout = 2 * TIMEOUT_RETRIEVE / SLEEP_READ;       // ct_6
   while (tout > 0) {                              // ct_7
      micro_sleep(SLEEP_READ);                     // ct_8
      read_server_data(ca);                        // ct_9
      if (ca->state != STATE_WAIT_REPLY) return;   // ct_10
      tout--;                                      // ct_11
   }                                               // ct_12
   ca->state = STATE_TRANS_TIMEOUT;                // ct_13
}

Повторно-входимая функция client_transaction(ca) является прикладным интерфейсом (API) асинхронных транзакций клиента. После ее выполнения приложение должно проверить статус транзакции. Если он показывает нормальное завершение - STATE_OK, это означает, что данные сервера были успешно приняты и могут использоваться приложением.

После успешного запроса данных у сервера [ct_4], [ct_5] функция переходит к выполнению монитора ожидания данных от сервера (от [ct_7] до [ct_12]). Этот монитор снабжен собственным контролем тайм-аута транзакции. Его значение устанавливается примерно вдвое большим штатного тайм-аута ожидания данных сервера [ct_6]. Необходимость такого решения обусловлена тем, что асинхронная транзакция может быть активирована в моменты времени, когда тайм-аут контроль блокирован. Если именно для этой транзакции произойдет сбой в получении данных от сервера, она будет постоянно находиться в состоянии ожидания ответа, поскольку статус транзакции при обращении к функции чтения read_server_data(ca) не будет изменяться [ct_9], [ct_10]. То есть при отсутствии собственного контроля тайм-аута монитор транзакции может зациклиться. Конечно, временная точность основанного на задержках [ct_8] собственного контроля заметно ниже таймерной, но и ситуации, когда возникают такие события, весьма редки. А для того, чтобы конечное приложение могло идентифицировать собственный тайм-аут транзакции клиента, используется отдельное значение статуса завершения [ct_13].

Отметим, что механизм подсчета пропущенных циклов контроля тайм-аута (см. функцию client_control()) как раз и срабатывает, когда новая транзакция клиента активируется во время блокировки тайм-аут контроля в прерываемой транзакции. Значение тайм-аута чтения полученных данных TIMEOUT_READ выбрано достаточно большим именно для того, чтобы прерванная транзакция могла бы завершиться успешно даже в случае однократного возникновения собственного тайм-аута вложенной транзакции.

void client_multi_transaction(clientappl *ca, unsigned16 npar)   // cm_0
{
   unsigned16 cnt, flag;
   unsigned32 tout;

   if (npar == 0) return;                                            // cm_5
   if (sem_reset >= 0) {                                             // cm_6
      ca->state = STATE_UNABLE;                                      // cm_7
      return;
   }
   for (cnt = 0; cnt < npar; cnt++) request_transaction(&ca[cnt]);   // cm_10
   tout = 2 * TIMEOUT_RETRIEVE / SLEEP_READ;                         // cm_11
   do {                                                              // cm_12
      micro_sleep(SLEEP_READ);
      flag = 0;                                                      // cm_14
      for (cnt = 0; cnt < npar; cnt++) {                             // cm_15
         if (ca[cnt].state == STATE_WAIT_REPLY) {                    // cm_16
            read_server_data(&ca[cnt]);                              // cm_17
            if (ca[cnt].state == STATE_WAIT_REPLY) flag = 1;         // cm_18
         }
      }
      tout--;                                                        // cm_21
      if (tout == 0) {                                               // cm_22
         for (cnt = 0; cnt < npar; cnt++) {                          // cm_23
            if (ca[cnt].state == STATE_WAIT_REPLY) {                 // cm_24
               ca[cnt].state = STATE_TRANS_TIMEOUT;                  // cm_25
            }
         }
         return;                                                     // cm_28
      }
   } while (flag);                                                   // cm_30
}

Функция client_multi_transaction(ca, npar) является аналогом client_transaction(ca) для многопараметрической транзакции клиента и приведена в качестве примера. Здесь используется не обязательное решение, когда в моменты блокировки тайм-аут контроля активация асинхронной транзакции не производится и возвращается статус невозможности ее выполнения [cm_6], [cm_7]. Вместе с тем здесь также используется собственный контроль тайм-аута [cm_11], [cm_21], [cm_22]. После запроса у сервера всех данных [cm_10] функция переходит к выполнению монитора транзакции - цикл [cm_12]. Основанием для выхода из цикла и завершения транзакции является наступление одного из двух событий: изменение статуса всех параметров транзакции либо собственный тайм-аут. Статус транзакции контролируется в цикле [cm_15] для каждого параметра, который все еще находится в состоянии ожидания ответа сервера [cm_16]. Если после обращения к функции чтения [cm_17] статус хотя бы одного такого параметра остается неизменным, устанавливается флаг продолжения мониторинга [cm_18]. При наступлении собственного тайм-аута транзакции [cm_21], [cm_22] для всех параметров, не вышедших из состояния ожидания ответа сервера, устанавливается статус тайм-аута цикла ожидания, после чего выполнение транзакции завершается [cm_28]. При использовании многопараметрических транзакций может потребоваться увеличение времени тайм-аутов, особенно когда возможен запрос многих параметров у одного сервера. Собственный тайм-аут [cm_11] также целесообразно увеличивать в зависимости от числа параметров транзакции.

void periodic(void)   // pr_0
{
   if (sleep_cnt > 0) sleep_cnt--;   // pr_2
   client_control();                 // pr_3
}       

Функция periodic() активируется периодическим таймером и занимается решением двух задач. В [pr_2] обслуживается счетчик времени задержки, а в [pr_3] вызывается функция контроля тайм-аута. Сигнал таймера должен обладать высоким приоритетом и блокировать остальные сигналы на время своего выполнения.

void init_client(void)   // ic_0
{
   unsigned16 cnt;

   sleep_cnt = 0;
   sem_reset = 0;    // ic_5
   flag_reset = 0;   // ic_6
   for (cnt = 0; cnt < SIZE_CLTWORK; cnt++) resetwork(cltwork + cnt);   // ic_7
   sem_reset = -1;   // ic_8
}

Функция (пере)инициализации init_client() сбрасывает счетчик просчетов тайм-аута [ic_6] и буферы транзакций [iс_7]. Затем открывается семафор тайм-аута [ic_8]. На время переинициализации контроль тайм-аута блокируется семафором sem_reset [ic_5]. Вызов init_client() должен выполняться только извне по отношению к функциям транзакции клиента, например, из монитора прикладной программы. Транзакции, активированные во время прохождения переинициализации, либо не смогут начать выполнение (решение [cm_6]), либо будут проходить без штатного контроля тайм-аута.