Доброго времени суток, Хабр.

Приходилось ли вам задумываться как поменять время внутри базы данных? Легко? Ну в некоторых случаях да, несложно — linux команда date и дело в шляпе. А если нужно поменять время только внутри одного экземпляра бд если их на сервере несколько? А для отдельно взятого процесса базы данных? А? Эээ, так-то, дружок, в этом-то все и дело. Кто-то скажет, что это очередной сюр, не связанный с реальностью, который периодически выкладывают на Хабре. Но нет, задача вполне реальная и продиктована производственной необходимостью — тестированием кода. Хотя соглашусь, кейс тестирования может быть достаточно экзотический — проверить как ведет себя код на определенную дату в будущем. В данной статье я рассмотрю подробно как эта задача решалась, и заодно захватим немного сам процесс организации тестовых и dev-стендов для базы Oracle. Впереди длинное чтиво, устраивайтесь поудобнее и прошу под кат.

Предпосылки


Начнем с небольшого вступления для того, чтобы показать зачем это нужно. Как уже было анонсировано, мы пишем тесты при реализации правок в базе данных. Система, под которую эти тесты делаются, разрабатывалась в начале(а может и слегка до начала) нулевых, поэтому вся бизнес-логика находится внутри базы данных и написана в виде хранимых процедур на языке pl/sql. И да, это приносит нам боль и страдания. Но это legacy, и с этим приходится жить. В коде и табличной модели есть возможность задавать то, как параметры внутри системы эволюционируют с течением времени, проще говоря задавать активность с какой даты и по какую дату они могут применяться. Чего далеко ходить — недавнее изменение ставки НДС яркий тому пример. И чтобы такие изменения в системе можно было проверить заранее, базу данных с такими правками нужно перенести на определенную дату в будущее, кода параметры в таблицах станут активными на «текущий момент». И ввиду специфики поддерживаемой системы нельзя использовать мок-тесты, которые бы просто изменили возвращаемое значение текущей системной даты в языке при запуске сессии тестирования.

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

Каменный век


Давно-давно, когда деревья были маленькими, а мейнфреймы большими, был только 1 сервер для разработки и он же проведения тестов. И в принципе всем этого хватало(640К хватит всем!)

Минусы: для проведения задачи смены времени нужно было привлекать много смежных отделов — системных администраторов(делали смену времени на сервере субд от root), администраторов СУБД(делали перезапуск базы данных), программистов(нужно было уведомить что произойдет смена времени, потому что часть кода переставала работать, например, веб-токены, ранее выданные для вызова api методов, переставали быть валидными и это могло стать неожиданностью), тестировщиков(само проведение тестирования)… При возврате времени в настоящее все повторялось в обратном порядке.

Средние века


Со временем число разработчиков в отделе росло и в какой-то момент 1 сервера хватать перестало. В основном из-за того, что разные разработчики хотят менять один и тот же pl/sql пакет и проводить для него тестирование(даже и без смены времени). Все чаще были слышны возмущения: «Доколе! Хватит это терпеть! Фабрики — рабочим, землю — крестьянам! Каждому программисту — по базе данных!» Однако, если у вас продуктовая база данных несколько терабайт, а разработчиков 50-100, то положа руку на сердце прямо в таком виде требование не очень-то реальное. А еще все хотят чтобы тестовая и dev-база не очень сильно отставали от прода и по структуре, и самим данным внутри таблиц. Так появился отдельный сервер для тестирования, назовем его pre-production. Строился он из 2х одинаковых серверов, куда с прода делалось восстановление базы данных из RMAN бакапов и занимало это примерно 2-2.5 дня. После восстановления в базе делалось обезличивание персональных и других важных данных и на данный сервер подавалась нагрузка с тестовых приложений(а также и сами программисты всегда работали с недавно восстановленным сервером). Обеспечение работы с нужным сервером происходило с помощью кластерного ip-ресурса, поддерживаемого через corosync(pacemaker). В то время как все работают с активным сервером, на 2 ноде запускается снова восстановление базы данных и через 2-3 дня они опять меняются местами.

Из очевидных минусов: нужно 2 сервера и в 2 раза больше ресурсов(в основном дисковых), чем прод.

Плюсы: операция смены времени и проведение тестов — может проводиться на 2м сервере, на основном сервере в это время живут разработчики и занимаются своими делами. Смена серверов происходит только когда база готова, и даунтайм тестовой среды минимален.

Эпоха научно-технического прогресса


При переходе на базу данных 11g Release 2 мы вычитали про интересную технологию, которую предоставляет Оракл под названием CloneDB. Суть заключается в том, что бакапы продуктовой базы данных(там прямо битовая копия продуктовых датафайлов) складируются на специальный сервер, который потом через DNFS(direct NFS) публикует этот набор датафайлов на в принципе любое число серверов, при этом на сервере не требуется иметь тот же объем дисков, потому что реализуется подход Copy-On-Write: база использует для чтения данных в таблицах сетевую шару с датафайлами с сервера бакапов, а изменения пишутся в локальные датафайлы на самом dev-сервере. Периодически для сервера делается «обнуление сроков», чтобы локальные датафайлы не очень сильно росли и место не кончалось. При обновлении сервера также делается деперсонализация данных в таблицах, при этом все обновления таблиц попадают в локальные датафайлы и те таблицы читаются с локального сервера, все остальные таблицы читаются по сети.

Минусы: серверов все так же 2(для обеспечения плавности обновления с минимальным простоем для потребителей), но зато теперь объем дисков сильно сокращается. Для хранения бакапов на nfs-шаре нужно еще 1 сервер по размеру +- как прод, но сокращается само время выполнения обновлений(особенно при использовании инкрементальных бакапов). Работа по сети с nfs-шарой заметно замедляет IO-операции чтения. Для использования технологии CloneDB база должна быть редакции Enterprise Edition, в нашем случае нам нужно было проводить на тестовых базах всякий раз процедуру upgrade. К счастью, тестовые базы попадают в исключения по лицензионной политике Oracle.

Плюсы: операция восстановления базы из бакапа занимает меньше 1 дня(точное время уже не вспомню).

Смена времени: без особых изменений. Хотя к этому времени уже были сделаны скрипты для изменения времени на сервере и рестарта базы данных, чтобы выполнять это не привлекая внимания санитаров администраторов.

Эра Новой истории


Для того чтобы еще больше экономить дискове пространство и сделать чтения данных не по сети мы решили реализовать свой вариант CloneDB(с флешбеком и снапшотами), используя файловую систему со сжатием. Во время проведения предварительных тестов выбор пал на ZFS, хотя официальной поддержки для неё нет в ядре Linux(цитата из статьи). Для сравнения смотрели еще BTRFS(b-tree fs), которую продвигает сам Оракл, но коэффициент сжатия был меньше при сравнительно одинаковом же потреблении CPU и RAM в тестах. Для включения поддержки ZFS на RHEL5 было собрано свое ядро на основе UEK(unbreakable enterprise kernel), а на более новых осях и ядрах можно просто использовать уже готовое UEK ядро. В основе реализации такой тестовой базы тоже лежит механизм COW, но уже на уровне снапшотов файловой системы. На сервер подается 2 дисковых устройства, на одном делается zfs pool, куда через RMAN делается дополнительный standby базы данных с прода, и поскольку мы используем сжатие, то раздел занимает меньше, чем продакшн.
На второе дисковое устройство ставится система и остальное что нужно для работы сервера и самой базы, например разделы для undo и temp. В любой момент времени из zfs пула можно сделать снапшот, который потом открывается как отдельная база данных. Создание снапшота занимает пару секунд. It's magic! И таких баз данных можно наклонировать в принципе достаточно много, лишь бы на сервере хватало RAM под все экземпляры и самого размера zfs pool(под хранение изменений в датафайлах при деперсонализации и при жизненном цикле клона базы данных). Основное время обновления тестовой базы начинает занимать операция деперсонализации данных, но и она укладывается в 15-20 минут. Налицо значительное ускорение.

Минусы: на сервере нельзя поменять время просто переводом системного времени, потому что тогда сразу все экземпляры базы данных, работающие на этом сервере, попадут в это время. Решение для этой проблемы было найдено и будет описано в соответствующим разделе. Забегая вперед скажу, что оно позволяет менять время внутри только 1 экземпляра базы данных(подход смены времени per instance) при этом не затрагивая остальные на этом же сервере. И время на самом сервере тоже не меняется. Так отпадает необходимость в рутовом скрипте для смены времени на сервере. Также на этом этапе реализована автоматизация смены времени для экземпляров через Jenkins CI и пользователям(условно говоря команды разработчиков), которые владеют своим стендом, даны права на джобы, через которые они могут сами менять время, обновлять стенд до актуального состояния с прода, делать снапшоты и восстановление(откат) базы до ранее созданного снапшота.

Эра Новейшей истории


С приходом Oracle 12c появилась новая технология — pluggable databases и как следствие контейнерные базы данных(cdb). С этой технологией внутри 1 физического экземпляра можно сделать несколько «виртуальных» баз, которые разделяют общую область памяти экземпляра. Плюсы: можно сэкономить память для сервера(и повысить общую производительность нашей базы данных, потому что всю память, которую занимали до того например 5 разных экземпляров, можно отдать в общее пользование для всех развернутых pdb-контейнеров внутри cdb, и они будут ее использовать только когда им это реально понадобится, а не так как было на предыдущей фазе, когда каждый экземпляр «блокировал» выданную ему память под себя и при низкой активности какого-то из клонов память не использовалась эффективно, проще говоря простаивала). Датафайлы разных pdb все также лежат в zfs pool и при разворачивании клонов используют все тот же мех-м zfs-снапшотов. На этом этапе мы приблизились достаточно близко к возможности давать почти каждому разработчику свою базу данных. Смена времени на этом этапе не требует перезапуска базы данных и работает совсем точено только для тех процессов, которым смена времени необходима, все остальные пользователи, работающие с этой бд никак не затрагиваются.

Минус: нельзя использовать подход со сменой времени per instance из предыдущей фазы, потому что инстанс у нас сейчас один. Однако, решение и для этого случая было найдено. И именно оно и послужило толчком для написания это статьи. Забегая вперед, скажу, что оно представляет собой подход смены времени per process т.е. в каждом процессе базы данных можно установить вообще свое уникальное время.

Типовая сессия тестирования в этом случае сразу после подключения к бд задает в начале своей работы нужное время, проводит тесты и в конце возвращает время обратно. Возврат времени нужен по одной простой причине: часть процессов базы Оракл не завершаются при дисконнекте клиента базы данных от сервера, это серверные процессы под названием shared servers, которые в отличие от dedicated процессов, запускаются при старте сервера базы данных и живут практически бесконечно(в идеальной картине мира). Если оставить время измененным в таком серверном процессе, то потом другое подключение, которое в этом процессе будет обслуживаться, будет получать неправильное время.

В нашей системе shared servers используются много, т.к. до 11g адекватного решения для нашей системы выдерживать highload нагрузку практически не существовало(в 11g появился DRCP — database resident connection pooling). И вот почему — в субд есть лимит на общее число серверных процессов, которое она может создать как в dedicated, так и в shared режиме. Dedicated процессы порождаются медленнее, чем база может выдать уже готовый shared процесс из пула разделяемых процессов, а значит при постоянном поступлении новых подключений(особенно если процесс делает еще какие-то медленные операции) общее число процессов будет расти. При достижении лимита сессий/процессов база данных перестает обслуживать новые соединения и наступает коллапс. Переход на использование пула разделяемых процессов позволил уменьшить число возникающих новых на сервере процессов при подключениях.

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

Подход «fake per instance»


Как поменять время внутри базы данных?

Первое что приходило в голову — создать в схеме, которая содержит весь код бизнес логики, свою функцию, которая перекрывает функции языка, работающие с временем(sysdate, current_date и т.д.) и при определенных условиях начинает давать другие значения, например можно было задавать значения через контекст сессии в начале запуска тестов. Не вышло, встроенные функции языка не перекрыть пользовательскими.

Затем были проверены системы легкой виртуализации (Vserver, OpenVZ) и контейнеризации через docker. Тоже не работает, они используют то же самое ядро что и хост-система, а значит используют те же значения системного таймера. Снова отпадает.

И тут нам на помощь приходит не побоюсь этого слова гениальное изобретение мира Linux — переопределение/перехват функций на этапе динамической загрузки shared objects. Многим оно известно как трюки с LD_PRELOAD. В переменной окружения LD_PRELOAD можно указать библиотеку, которая загрузится раньше всех остальных, которые нужны процессу, и если в этой библиотеке есть символы с тем же именем как например в стандартной libc, которая загрузится позднее, то таблица импорта символов для приложения будет выглядеть как если функцию предоставляет наш подменный модуль. И именно это и делает библиотека проекта libfaketime, которую мы стали использовать для того, чтобы запустить базу данных в другом времени отдельно от системного. Библиотека пропускает через себя вызовы, которые касаются работы с системным таймером и получения системного времени и даты. Управлять тем, на сколько сместится время относительно текущей даты сервера или от какого момента времени время должно пойти внутри процесса — все управляется переменными окружения, которые нужно задать вместе с LD_PRELOAD. Для реализации смены времени у нас был реализован job на сервере Jenkins, который заходит на сервер базы данных и перезапускает СУБД либо с установленными переменными окружения для libfaketime либо без них.

Примерный алгоритм запуска базы данных с подменным временем:

export LD_PRELOAD=/usr/local/lib/faketime/libfaketime.so
export FAKETIME="+1d"
export FAKETIME_NO_CACHE=1

$ORACLE_HOME/bin/sqlplus @/home/oracle/scripts/restart_db.sql

И если выдумаете, что все сразу заработало, то глубоко заблуждаетесь. Потому что оракл, как оказалось, валидирует те библиотеки, которые загружаются внутрь процесса при старте СУБД. И в алертлоге начинает возмущаться о замеченном подлоге, при этом база не стартует. Сейчас уже точно не помню как избавиться, есть какой-то параметр, которым можно отключить таки выполнение sanity-проверки при старте.

Подход «fake per process»


Общая идея с изменением времени только внутри 1 процесса оставалась той же — использовать libfaketime. Мы запускаем базу данных с предзагруженой в нее библиотекой, но ставим нулевое смещение времени при запуске, которое пропагандируется потом во все процессы СУБД. А потом внутри тестовой сессии выполнить установление переменной окружения только для этого процесса. Пфф, делов-то.

Однако, для тех кто близко знаком с pl/sql языком сразу понятна вся обреченность этой идеи. Потому что язык сильно ограничен и в основном годится для задач высокоуровневых. Никакого системного программирования там не реализовать. Хотя некоторые низкоуровневые операции(например работа с сетью, работа с файлами) присутствуют в виде предустановленых системных dbms/utl-пакетов. За все время работы с Ораклом я несколько раз делал реверс-инжиниринг предустановленных пакетов, код некоторых из них скрыт от глаз посторонних(они называются wrapped). Если тебе чего-то запрещают смотреть, то соблазн узнать как оно устроено внутри только возрастает. Но часто даже после анвраппера не всегда есть что посмотреть, потому что функции таких пакетов реализуются как c interface к so-библиотекам на диске.
Итого, мы подошли к 1 кандидату на реализацию — технология c external procedures.
Оформленная специальным образом библиотека может экспортировать методы, которые потом база оракл может вызывать через pl/sql. Кажется многообещающим. Всего один раз я встречался с этим на курсах по Advanced plsql, поэтому помнил очень отдаленно как это готовить. И это значит нужно читать документацию оракла. Прочитал — и сразу приуныл. Потому что загрузка такой пользовательской so-библиотеки идет в отдельном процессе-агенте через листенер базы данных, и общение с этим агентом идет через дблинк. А значит плакала наша идея установить переменную окружения внутри самого процесса базы данных. И сделано это все из соображений безопасности.

Картинка из документации, которая показывает как это работает:



Тип библиотеки so/dll — не так важно, но картинка почему-то только под винду.

Возможно кто-то заметил тут еще 1 потенциальную возможность. Да-да, это Java. Оракл позволяет писать код хранимых процедур не только на plsql, но и на java, которые тем не менее экспортируются все равно как plsql методы. Периодически я делал такое, поэтому с этим проблем возникнуть не должно. Но тут был спрятан очередной подводный камень. Java работает с копией энвайромента, и позволяет только получать переменные окружения какие были у JVM процесса при старте. Встроенная JVM наследует переменные окружения процесса базы данных, но на этом все. Я видел в интернете советы как поменять readonly map через reflection, но что толку, ведь это все равно только копия. То есть баба осталась опять у разбитого корыта.

Однако Java это не только ценных мех. С ее помощью можно порождать процессы изнутри процесса базы данных. Хотя все небезопаные операции нужно разрешать отдельно через механизм java grants, которые делаются с помощью пакета dbms_java. Изнутри plsql кода можно получить process pid текущего серверного процесса, в котором код выполняется, с помощью системных представлений v$session и v$process. Дальше мы можем породив какой-то дочерний процесс от нашей сессии что-то с этим pid сделать. Для начала я вывел просто все переменные окружения, которые есть внутри процесса базы данных(для проверки гипотезы)

#!/bin/sh

pid=$1

awk 'BEGIN {RS="\0"; ORS="\n"} $0' "/proc/$pid/environ"

Ну вывел, а дальше-то что. Поменять переменные в файле environ все равно нельзя, это данные которые были переданы в процесс при его старте и они read only.

Я поискал решение в Интернете на stackoverflow «как поменять переменную окружения в другом процессе». Большая часть ответов была что это невозможно, но был один ответ, который описывал эту возможность как нестандартный и грязный хак. И этим ответом был Альберт Эйнштейн gdb. Отладчиком можно прицепиться на любой процесс зная его pid и выполнить в нем любую функцию/процедуру, которая в нем существует как публично экспортируемый символ, например из какой-то библиотеки. В libc есть функции работы с переменными окружения и libc загружается в любой процесс базы Оракл(да и практически всякой программы на linux).

Вот так реализуется установление переменной окружения в чужом процессе(вызывать нужно от root из-за используемого сискола ptrace):

#!/bin/sh

pid=$1
env_name=$2
env_val="$3"

out=`gdb -q -batch -ex "attach $pid" -ex 'call (int) setenv("'$env_name'", "'"$env_val"'", 1)' -ex "detach" 2>&1`


Также и чтобы посмотреть переменные окружения внутри процесса gdb тоже годится. Как было уже сказано ранее файл environ из /proc/pid/ показывает только переменные, которые существовали на старте процесса. А если процесс создал что-то в ходе своей работы, то это увидеть можно только через отладчик:
#!/bin/sh

pid=$1
var_name=$2

var_value=`gdb -q -batch -ex "attach $pid" -ex 'call (char*) getenv("'$var_name'")' -ex 'detach' | egrep '^\$1 ='`

if [ "$var_value" == '$1 = 0x0' ]
then
  # variable empty or does not exist
  echo -n
else
  # gdb returns $1 = hex_value "string value"
  var_hex=`echo "$var_value" | awk '{print $3}'`
  var_value=`echo "$var_value" | sed -r -e 's/^\$1 = '$var_hex' //;s/^"//;s/"$//'`
  
  echo -n "$var_value"
fi


Итак, решение уже у нас в кармане — через яву мы спауним процесс отладчика, который заходит на породивший его процесс и ставит ему нужную переменную окружения и потом завершается(мавр сделал свое дело — мавр может уйти). Но было ощущение, что оно какое-то костыльное. Хотелось чего-то более элегантного. Как-то бы все-таки заставить сам процесс базы данных устанавливать себе переменные окружения без внешнего рукоприкладства.

Яйцо в утке, утка в зайце...


И тут на помощь нам приходит кто, да, вы правильно догадались, снова Java, а именно JNI(java native interface). JNI подзволяет вызывать native методы, написаные на си, внутри JVM. Код оформляется специальным образом в виде shared object библиотеки, которую потом загружает JVM, при этом методы, которые были в библиотеке, мапятся на методы java внутри класса, объявленные с модификатором native.

Ну ок, пишем класс(на самом деле это только заготовка):

public class Posix {

    private static native int setenv(String key, String value, boolean overwrite);

    private static native String getenv(String key);
    
    public static void stub() 
    {
        
    }
}

После этого компилируем его и получаем сгенерированый h-файл будущей библиотеки:

# компилируем исходник
javac Posix.java

# создаст файл Posix.h в котором описаны декларации нативных методов для JNI
javah Posix

Получив заголовочный файл напишем тело для каждого метода:

#include <stdlib.h>
#include "Posix.h"

JNIEXPORT jint JNICALL Java_Posix_setenv(JNIEnv *env, jclass cls, jstring key, jstring value, jboolean overwrite)
{
    char* k = (char *) (*env)->GetStringUTFChars(env, key, NULL);
    char* v = (char *) (*env)->GetStringUTFChars(env, value, NULL);

    int err = setenv(k, v, overwrite);

    (*env)->ReleaseStringUTFChars(env, key, k);
    (*env)->ReleaseStringUTFChars(env, value, v);

    return err;
}

JNIEXPORT jstring JNICALL Java_Posix_getenv(JNIEnv *env, jclass cls, jstring key)
{
    char* k = (char *) (*env)->GetStringUTFChars(env, key, NULL);
    char* v = getenv(k);

    return (*env)->NewStringUTF(env, v);
}

и скомпилируем библиотеку

gcc -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -fPIC Posix.c -shared -o libPosix.so -Wl,-soname -Wl,--no-whole-archive

strip libPosix.so

Для того чтобы Java могла загрузить native-библиотеку, она должна быть найдена системным ld по всем правилам Linux. Дополнительно у Java есть набор property, которые содержат пути, где поиск библиотек происходит. Для работы внутри оракл проще всего положить нашу библиотеку в $ORACLE_HOME/lib.

И уже после того, как мы создали библиотеку, нужно скомпилировать класс внутри базы данных и опубликовать его как plsql пакет. Для создания ява классов внутри базы данных есть 2 варианта:

  • загрузить бинарный class-файл через утилиту loadjava
  • скомпилировать код класса из исходников с помощью sqlplus

Мы воспользуемся вторым способом, хотя они в принципе равноправны. Для первого случая нужно было сразу написать весь код класса на 1 этапе, когда мы получали класс-заглушку для h-файла.

Для создания ява класса в субд используется специальный синтаксис:

CREATE OR REPLACE AND RESOLVE JAVA SOURCE NAMED "Posix" AS
...
...
/

Когда класс создан, его нужно опубликовать как plsql методы, и тут опять специальный синтаксис:

procedure set_env(var_name varchar2, var_value varchar2)
is
language java name 'Posix.set_env(java.lang.String, java.lang.String)';

При попытке вызова потенциально небезопасных методов внутри Java возбуждается эксепшн, который говорит, что не выдан java grant для пользователя. Загрузка native методов — это еще какая небезопасная операция, потому что мы инжектируем посторонний код прямо внутрь процесса базы данных(тот самый эксплойт, который был анонсирован в заголовке).

Но поскольку база тестовая, даем грант без особых опасений подключившись от sys:

begin
dbms_java.grant_permission( 'SYSTEM', 'SYS:java.lang.RuntimePermission', 'loadLibrary.Posix', '');
commit;
end;
/

Имя пользователя system — это тот, куда я компилировал код java и plsql wrapper package.
Важно заметить, что при загрузке библиотеки через вызов System.loadLibrary мы опускаем префикс lib и расширение so(то как это описано в документации) и не передаем никакой путь где искать. Есть аналогичный метод System.load, который может загрузить библиотеку только с использованием абсолютного пути.

И тут нас ожидает 2 неприятный сюрприз — я угодил в очередную кроличью нору Оракла. При выдаче гранта возникает ошибка с довольно туманным сообщением:

ORA-29532: Java call terminated by uncaught Java exception: java.lang.SecurityException: policy table update

Проблема гуглится в интернете и ведет на My Oracle Support (aka Metalink). Т.к. по правилам Оракл публиковать в открытых источниках статьи с металинка запрещено, то упомяну только номер документа — 259471.1 (те у кого есть доступ смогут прочитать сами).

Суть проблемы — в том что Оракл не даст нам просто так разрешить загрузку подозрительного стороннего кода себе в процесс. Что логично.

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

It's alive, alive


С замиранием сердца я решил попробовать вдохнуть жизнь в своего Франкенштейна.
Запускаем базу данных с предзагруженым libfaketime и 0 смещением
Подключаемся в базу и делаем вызов кода который просто выведет время до и после изменения переменной окружения:

begin
dbms_output.enable(100000);
dbms_java.set_output(100000);
dbms_output.put_line('old time: '||to_char(sysdate, 'dd.mm.yyyy hh24:mi:ss'));
system.posix.set_env('FAKETIME','+1d');
dbms_output.put_line('new time: '||to_char(sysdate, 'dd.mm.yyyy hh24:mi:ss'));
end;
/


Работает, черт подери! Честно сказать, я ждал каких-то еще сюрпризов, например ORA-600 ошибок. Однако в алертлоге было все число и код продолжал работать.
Важно отметить, что если подключение в базу делается как dedicated, то после завершения соединения процесс уничтожится и никаких следов не останется. Но если мы используем shared соединения, то в этом случае из серверного пула выделяется уже готовый процесс, мы в нем меняем время через переменные окружения и при отключении оно внутри процесса так и останется измененным. И когда потом другая сессия базы данных попадает в тот же серверный процесс, она будет получать неправильное время к своему немалому удивлению. Поэтому при завершении сессии тестов лучше всегда возвращать время обратно к нулевому смещению.

Заключение


Надеюсь, что рассказ был интересным(и может даже кому-то полезным).

Исходные коды все доступны на Github.

Документация по libfaketime — тоже.

А как вы делаете тестирование? И как создаете dev- и test-базы в компании?

Бонус для дочитавших до конца