Исследование проблемы зависания PL/SQL с SYSTIMESTAMP в Docker-контейнере

1. Описание проблемы

При выполнении PL/SQL-кода из Docker-контейнера наблюдается зависание на неопределённый срок:

v_start TIMESTAMP := SYSTIMESTAMP;
LOOP
    EXIT WHEN SYSTIMESTAMP >= v_start + NUMTODSINTERVAL(1, 'SECOND');
    NULL;
END LOOP;

Среда выполнения:
- Хост (работает): CentOS/RHEL, glibc 2.17/2.28, Python 3.9, oracledb 3.4.2, Oracle Instant Client 12.2
- Контейнер (зависает): apache/airflow:2.10.5 на Debian bookworm, glibc 2.36, Python 3.12, oracledb 3.4.2, Oracle Instant Client 19.31

Сервер БД: Oracle Database 12c Enterprise Edition Release 12.1.0.2.0. Один и тот же в обоих случаях.

Проявление: пакет TSC_MAJOR_V2.send_soap_request перестаёт отвечать при запуске из Docker. Процедура содержит цикл ожидания (busy-wait) с использованием SYSTIMESTAMP. Все процедуры, вызывающие send_soap_request, зависают — update_events, update_cities, airflow_run_registry.


2. Диагностика

2.1. Методика

Проведено пошаговое тестирование каждого компонента send_soap_request изолированно. Все тесты выполнялись в отдельных соединениях с таймаутом 8 секунд (подпроцессный запуск).

2.2. Результаты

Компонент Результат в контейнере
SELECT 1 FROM dual ✅ 0.0s
DBMS_LOCK.SLEEP(2) ✅ 2.0s
UTL_HTTP запрос к API ✅ 0.1s
TIMESTAMP := SYSTIMESTAMP + LOOP ❌ зависание
TIMESTAMP WITH TIME ZONE := SYSTIMESTAMP + LOOP ✅ 1.0s
LOCALTIMESTAMP + LOOP ✅ 1.0s
DATE := SYSDATE + 1/86400 LOOP ✅ 0.3s
UTL_HTTP после TIMESTAMP loop (default TZ) ❌ зависание
UTL_HTTP после TIMESTAMP loop (TZ=Europe/Moscow) ✅ 1.1s

2.3. Проверенные версии

Нижеприведённые комбинации не влияют на наличие проблемы — все воспроизводят зависание в контейнере:

  • oracledb: 2.4.1, 3.4.2, 4.0.1
  • Oracle Instant Client: 12.2, 19.19, 21.16
  • Python: 3.9, 3.12
  • Базовый образ: Debian bookworm (glibc 2.36), oraclelinux:8-slim (glibc 2.28)

Базовые образы с glibc 2.28 и 2.36 воспроизводят зависание. На хосте с glibc 2.17/2.28 проблема отсутствует.


3. Причина

3.1. Технический механизм

  1. SYSTIMESTAMP возвращает тип TIMESTAMP WITH TIME ZONE.
  2. Переменная v_start объявлена как TIMESTAMP — тип без часового пояса.
  3. Присваивание v_start := SYSTIMESTAMP вызывает неявное преобразование с потерей часового пояса.
  4. Сравнение SYSTIMESTAMP >= v_start + NUMTODSINTERVAL(...) требует приведения TIMESTAMP к TIMESTAMP WITH TIME ZONE (или наоборот).
  5. Процедура приведения типов в Oracle использует системные вызовы timezone-преобразования.
  6. В glibc 2.36 эти вызовы приводят к блокировке процесса.
  7. Блокировка не снимается стандартными механизмами таймаутов.

3.2. Факторы

  • glibc 2.36. Основной фактор. В более старых версиях (2.17, 2.28) блокировка не возникает.
  • Неявное преобразование типов. Инициирует вызов системной функции timezone-преобразования.
  • Docker-окружение. Контейнеризация может влиять на доступ к timezone-данным и системному времени, однако базовой причиной является поведение glibc.
  • Busy-wait. Цикл ожидания с SYSTIMESTAMP не является штатным механизмом задержки в PL/SQL.

3.3. Условия возникновения

Проблема воспроизводится при совпадении всех трёх факторов:

  1. Использование TIMESTAMP := SYSTIMESTAMP с последующим сравнением
  2. Версия glibc >= 2.28 (подтверждено на 2.28 и 2.36)
  3. Запуск в Docker-контейнере

4. Решение

4.1. Основной метод

Установка часового пояса сессии перед вызовом процедур, содержащих проблемный код:

cur.execute("ALTER SESSION SET TIME_ZONE = 'Europe/Moscow'")

Данный метод устраняет необходимость неявного преобразования типов, так как Oracle интерпретирует TIMESTAMP в заданном часовом поясе, не вызывая системную функцию timezone-преобразования.

4.2. Альтернативные способы

При невозможности изменения сессии можно использовать корректные типы данных:

-- Вариант 1. Явное указание типа с часовым поясом
v_start TIMESTAMP WITH TIME ZONE := SYSTIMESTAMP;

-- Вариант 2. Использование LOCALTIMESTAMP (чистый TIMESTAMP)
v_start TIMESTAMP := LOCALTIMESTAMP;

-- Вариант 3. Использование DATE
v_start DATE := SYSDATE;

4.3. Рекомендация

Штатным механизмом задержки в PL/SQL является DBMS_LOCK.SLEEP(n). Он не нагружает CPU, не зависит от системного времени и timezone, и не вызывает проблем с glibc. Busy-wait с SYSTIMESTAMP не рекомендуется к использованию.


5. Выводы

  1. TIMESTAMP := SYSTIMESTAMP с последующим сравнением типов TIMESTAMP и TIMESTAMP WITH TIME ZONE вызывает неявное преобразование, которое на glibc 2.36 приводит к блокировке процесса.
  2. Проблема не зависит от версий Oracle Instant Client, oracledb, Python или образа Docker — определяющим фактором является версия glibc.
  3. Решение — ALTER SESSION SET TIME_ZONE = 'Europe/Moscow' перед вызовом процедур с проблемным кодом.
  4. Для задержек в PL/SQL следует использовать DBMS_LOCK.SLEEP(n) — это безопасный и предсказуемый механизм.