Problem
Biorę udział w utrzymywaniu pewnego systemu typu ODS. Jednym z poważniejszych problemów podczas wgrywania poprawek jest sprawa zależności między pakietami. Przypuśćmy, że trzeba zmienić pakiet A, od którego zależą pakiety B1 i B2, które są wykorzystywane w ramach jakiejś sesji. Zmiana w A powoduje, że najbliższe uruchomienia pakietów B1 lub B2 bez tworzenia nowej sesji zakończą się błędem “ORA-04061: existing state of package has been invalidated”, jeśli tylko w A są jakiekolwiek zmienne globalne. To zachowanie Oracle’a wynika z faktu, że zmienne globalnych pakietów trwają przez cały czas życia sesji. Przy zmianie kodu pakietu mogły się one zmienić, dlatego muszą zostać zainicjowane na nowo. Ponieważ zresetowanie zmiennych w czasie trwania sesji mogłoby prowadzić do niespójności – Oracle rzuca opisany powyżej wyjątek. Ten mechanizm jest dokładniej opisany na stronie
http://oraclequirks.blogspot.com/2007/03/ora-04061-existing-state-of-package-has.html Zmiana pakietu – zarówno specyfikacji jak i ciała pakietu – nie jest możliwa jeśli coś z niego korzysta – w takim wypadku komenda zastąpienia pakietu przez nową wersję ( CREATE OR REPLACE PACKAGE [BODY]) czeka aż pakiet przestanie być używany. Ale wystarczy, aby jakiś nowy proces zaczynał korzystać z pakietu zanim stary przestanie to robić i następuje zagłodzenie. Nie można wgrać nowej wersji pakietu. Takie zjawiska bardzo utrudniają wdrażanie, trzeba zabijać procesy korzystające ze zmienianego pakietu. Wypracowałem nawet skrypt, który zabija wszystkie sesje blokujące jakąś sesję uruchomioną z mojego komputera i mojego użytkownika systemu operacyjnego:
DECLARE CURSOR c_sql_hist(pp_machine VARCHAR2, pp_osuser VARCHAR2) IS SELECT ash.blocking_session, ash.blocking_session_status, ash.blocking_session_serial#, s.program, s.module, s.action FROM v$active_session_history ash JOIN v$session s ON ash.blocking_session = s.sid AND ash.blocking_session_serial# = s.serial# WHERE ash.session_id in ( select sid from v$session where machine = pp_machine and osuser = pp_osuser ) AND ash.sample_time > SYSDATE - 2 / 24 / 3600 ORDER BY sample_time DESC; v_r c_sql_hist%ROWTYPE; v_my_machine v$session.MACHINE%TYPE; v_my_osuser v$session.osuser%TYPE; BEGIN select machine, osuser into v_my_machine, v_my_osuser from v$session where sid = ( Select Sid from v$mystat where rownum = 1); for i in 1..10000 loop -- just to prevent infinite execution dbms_output.put_line('step: ' || i ); OPEN c_sql_hist(v_my_machine, v_my_osuser); FETCH c_sql_hist INTO v_r; IF NOT c_sql_hist%NOTFOUND THEN dbms_output.put_line(v_r.blocking_session || ' ' || v_r.blocking_session_status || ' ' || v_r.blocking_session_serial# || ' ' || v_r.program || ' ' || v_r.module || ' ' || v_r.action); execute immediate ' ALTER SYSTEM KILL SESSION ''' || v_r.blocking_session || ',' || v_r.blocking_session_serial# || ''''; else exit; END IF; CLOSE c_sql_hist; dbms_lock.sleep(0.5); -- sleep is used to get new blocking sessions end loop; END;
Opisane powyżej problemy z zależnościami doprowadziły do sytuacji, w których różne pakiety narzędziowe, które powinny być zgromadzone w jednym centralnym schemacie zostały skopiowane do wszystkich schematów. Dzięki temu można je zmieniać bez konieczności zabijania wszystkich procesów działających na bazie, ale jest to rozwiązanie fatalne z punktu widzenia utrzymania kodu.
Rozwiązanie
Stosowanym przez nas rozwiązaniem tego problemu jest, aby odwołania do pakietów odbywały się poprzez synonimy. Jeśli jest potrzeba zmienić pakiet – tworzymy jego nową wersję a po przetestowaniu przepina się synonim, aby na nią wskazywał. Najbliższe uruchomienie tego synonimu w ramach sesji powoduje, że uruchamiany jest nowy pakiet. Jego zmienne są inicjalizowane na nowo, ale nie jest to problem, ponieważ pełnią funkcje cache’a a nie służą do przechowywania stanu. Występuje dodatkowe ograniczenie, że nie mogą chodzić równocześnie dwie wersje tej samej procedury lub pakietu, których synonimy wskazują na odmienne obiekty. Dlatego jeśli uruchomi się B1, przepnie synonim na A_v2, po czym ponownie uruchomi B1 (pośrednio lub bezpośrednio) to to drugie uruchomienie B1 nie rozpocznie się dopóki pierwsze – na starym synonimie – się nie skończy. Nie jest to jednak praktyczny problem. Opisane obejście pozwala na sprawniejszy rozwój i wdrażanie kodu bazodanowego. Konieczna jest tylko świadomość działania mechanizmu i powyższych ograniczeń. Należało również wypracować praktykę nazewnictwa (przykładowo dodawanie na końcu nazwy TO_CHAR(sysdate, ‘$MM_DD’) czyli znaku dolara oraz miesiącu i dnia wdrożenia wersji). Konieczne jest również nadanie innym schematom tych samych praw do nowej wersji pakietu, które mieli do starej wersji.