После переустановки одного из двух связанных DBlink'ом серверов Oracle перестала работать связка из локальной и удаленной функций. Что случилось?
DBlink - прекрасное средство, позволяющее объединить два и более разделённых сетью серверов Oracle в единый ансамбль. Например, у нас есть боевой сервер, а мы бы хотели создать систему его мониторинга и отображения результатов наблюдений. Мы можем построить эту систему прямо на боевом сервере, но представляется более удобным иной подход.
Установим на отдельной машине отдельный сервер Oracle. Ему не потребуется особая мощность и мы можем абсолютно бесплатно и абсолютно легально взять стартовый Express Edition. Поднимем на нем Oracle APEX - и мы имеем готовую независимую среду разработки и эксплуатации сервера-наблюдателя с красивой и эффективной визуализацией. Создадим на сервере-наблюдателе DBlink на боевой сервер с необходимыми правами - и у нас всё готово, мы можем создавать систему мониторинга и визуализации:
create database link my_dblnk
connect to <remote_schema>
identified by <remote_password>
using <remote_host>/<remote_sid>
;
select * from remote_table@my_dblnk
;
- читаем данные из таблицы на удалённом сервере через сеть, адресуясь именем успешно созданного DBlink'а. И всё работает прекрасно, пока мы читаем данные из таблиц и представлений.
Но однажды нам понадобится перейти к хранимым программам, скорее всего, к функциям. И простейший и, вероятно, первый, с которым мы встретимся, пример - это функция взятия текущей даты (и времени) sysdate:
alter session set nls_date_format = 'dd.mm.yy hh24:mi:ss'
;
select sysdate from dual
;
28.10.20 19:22:38
Однажды нам понадобится получить значение sysdate с удаленного сервера. Причины тому разные, но наиболее вероятная - рассогласование часов на боевом сервере и сервере-наблюдателе. Мы попытаемся написать:
select sysdate@my_dblnk from dual
;
- и получим
ORA-00923: ключевое слово FROM не найдено там, где оно ожидалось
00923. 00000 - "FROM keyword not found where expected"
Попытаемся написать:
select sysdate from dual@my_dblnk
;
- и получим локальную дату (и время). Из этих двух наблюдений мы можем сделать вывод, что нельзя вызывать функции на удаленном сервере по DBlink'у - и ошибёмся. Можно. Но не sysdate. Всё, мы уперлись в серьёзную проблему: как взять дату (и время) на удаленном сервере?
Простейшее решение - написать-таки на удалённом сервере простейшую функцию, возвращающую значение даты (и времени):
create function now_date return date
is
begin
return sysdate;
end;
- и вызвать её по DBlink'у:
select sysdate, now_date@my_dblnk from dual
;
28.10.20 20:06:11 28.10.20 20:11:05
Мы решили смысловую задачу: первое значение - это локальная дата (и время), вторая - удалённая дата (и время). Попутно мы убедились, что вызывать по DBlink'у удалённые функции можно. Но не всякие. Но "простые" точно можно.
Но не всегда у нас есть возможность вести разработку на удалённом сервере, иногда мы можем лишь использовать его готовые объекты. В этом случае решение будет гораздо более сложным: нам придётся создать что-то на локальном сервере, что будет выполнять нужное нам действие на удалённом сервере. Я не буду переписывать предложение от Тома Кайта, оно доступно по следующей ссылке:
https://asktom.oracle.com/pls/apex/f?p=100:11:0::::P11_QUESTION_ID:50128786135391
(в скобках заметим, что Том ведёт дела, конечно же, в Oracle APEX, что видно по специфическому синтаксису ссылки)
Суть идеи состоит в том, чтоб написать локальную функцию, назовем ее test_remote_sysdate, которая будет по шагам открывать курсор по DBlink'у, парсить выражение по DBlink'у, связывать переменные по DBlink'у и т.д. Я не случайно повторил "DBlink" в каждой фразе - это принципиально важно. Смысловое PL/SQL выражение, которое мы будем запускать на удалённом сервере, выглядит следующим образом:
begin :x := sysdate; end;
- это анонимный PL/SQL блок, принимающий в переменную (бинд, плейсхолдер) значение текущей - на удалённом сервере - даты. Это по сути полный аналог рассмотренного выше подхода с функцией на удалённом сервере с той лишь разницей, что теперь функция располагается на локальном сервере, а на удалённый сервер в момент вызова локальной функции передается для исполнения смысловой анонимный блок. С оформлением всех этапов открытия курсора, парсинга, связывания и т.д. с помощью вызова функций стандартного пакета dbms_sql на удалённом сервере. Т.е. функция локально, действия удалённо, смысловой блок передаётся, условно говоря, параметром.
select sysdate, test_remote_sysdate from dual
;
28.10.20 20:34:43 28.10.20 20:39:37
Всё работает. До поры.
Однажды на удалённом сервере решат, например, обновить версию сервера Oracle. Экспортируют базу, заменят версию, импортируют базу. И наша локальная функция test_remote_sysdate, которая запускала удалённый процесс взятия даты, перестанет работать. Диагностика будет вещать что-то непонятное про ORA-04062 и time stamp. Первое, что мы, вероятно, подумаем, это что-то про дату - ведь наша функция и была создана для работы с датой. Но это не так.
Дело в том, что работа по DBlink'у по сути своей довольно сложна. Oracle скрывает от нас, берёт на себя огромное количество вспомогательных действий - но взамен выдвигает некоторые требования. И удалённый вызов функции в этом смысле отличается от чтения из таблиц или локального вызова функции (запишем по строкам, иначе путаница обеспечена):
При вызове из локальной функции удалённой функции...
дата компиляции вызываемой удалённой функции...
должна быть старше (меньше по значению)...
даты компиляции вызывающей локальной функции.
И это при вызове проверяется. Под угрозой отказа в исполнении.
Когда мы писали свою локальную функцию test_remote_sysdate, удалённый сервер - и его стандартные пакеты - уже существовали. А наша локальная функция test_remote_sysdate вызывает функции пакета dbms_sql на удалённом сервере для открытия курсора, парсинга и т.д. А вот когда удалённый сервер "переставили", то наша локальная функция "устарела" относительно вызываемых ей функций удалённого сервера. Он и отказал в исполнении. Решение простое: изменить дату компиляции локальной вызывающей функции - но опять "не слава Богу": формальная компиляция без изменения кода функции дату компиляции не меняет. Надо делать либо alter, либо, что проще, внести незначащее изменение в текст компилируемой функции - пробел там какой-нибудь, перевод каретки - и "по-честному" скомпилировать "изменившуюся" вызывающую локальную функцию.
Вот такие "внезапные" отказы на DBlink'ах. Это коварная проблема: она проявляется очень редко, в специфических условиях, на довольно навороченных и поживших системах. Возможно, это исследование поможет кому-то сэкономить нервы.