Rozwiązanie problemów z zależnościami pakietu poprzez synonimy

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.

You May Also Like

JCE keystore and untrusted sites

Recently at work I was in need of connecting to a web service exposed via HTTPS. I've been doing this from inside Servicemix 3.3.1, which may seem a bit inhibiting, but that was a requirement. Nevertheless I've been trying my luck with the included ser...Recently at work I was in need of connecting to a web service exposed via HTTPS. I've been doing this from inside Servicemix 3.3.1, which may seem a bit inhibiting, but that was a requirement. Nevertheless I've been trying my luck with the included ser...

Thought static method can’t be easy to mock, stub nor track? Wrong!

No matter why, no matter is it a good idea. Sometimes one just wants to check or it's necessary to be done. Mock a static method, woot? Impossibru!

In pure Java world it is still a struggle. But Groovy allows you to do that really simple. Well, not groovy alone, but with a great support of Spock.

Lets move on straight to the example. To catch some context we have an abstract for the example needs. A marketing project with a set of offers. One to many.

import spock.lang.Specification

class OfferFacadeSpec extends Specification {

    OfferFacade facade = new OfferFacade()

    def setup() {
        GroovyMock(Project, global: true)
    }

    def 'delegates an add offer call to the domain with proper params'() {
        given:
            Map params = [projId: projectId, name: offerName]

        when:
            Offer returnedOffer = facade.add(params)

        then:
            1 * Project.addOffer(projectId, _) >> { projId, offer -> offer }
            returnedOffer.name == params.name

        where:
            projectId | offerName
            1         | 'an Offer'
            15        | 'whasup!?'
            123       | 'doskonała oferta - kup teraz!'
    }
}
So we test a facade responsible for handling "add offer to the project" call triggered  somewhere in a GUI.
We want to ensure that static method Project.addOffer(long, Offer) will receive correct params when java.util.Map with user form input comes to the facade.add(params).
This is unit test, so how Project.addOffer() works is out of scope. Thus we want to stub it.

The most important is a GroovyMock(Project, global: true) statement.
What it does is modifing Project class to behave like a Spock's mock. 
GroovyMock() itself is a method inherited from SpecificationThe global flag is necessary to enable mocking static methods.
However when one comes to the need of mocking static method, author of Spock Framework advice to consider redesigning of implementation. It's not a bad advice, I must say.

Another important thing are assertions at then: block. First one checks an interaction, if the Project.addOffer() method was called exactly once, with a 1st argument equal to the projectId and some other param (we don't have an object instance yet to assert anything about it).
Right shit operator leads us to the stub which replaces original method implementation by such statement.
As a good stub it does nothing. The original method definition has return type Offer. The stub needs to do the same. So an offer passed as the 2nd argument is just returned.
Thanks to this we can assert about name property if it's equal with the value from params. If no return was designed the name could be checked inside the stub Closure, prefixed with an assert keyword.

Worth of  mentioning is that if you want to track interactions of original static method implementation without replacing it, then you should try using GroovySpy instead of GroovyMock.

Unfortunately static methods declared at Java object can't be treated in such ways. Though regular mocks and whole goodness of Spock can be used to test pure Java code, which is awesome anyway :)No matter why, no matter is it a good idea. Sometimes one just wants to check or it's necessary to be done. Mock a static method, woot? Impossibru!

In pure Java world it is still a struggle. But Groovy allows you to do that really simple. Well, not groovy alone, but with a great support of Spock.

Lets move on straight to the example. To catch some context we have an abstract for the example needs. A marketing project with a set of offers. One to many.

import spock.lang.Specification

class OfferFacadeSpec extends Specification {

    OfferFacade facade = new OfferFacade()

    def setup() {
        GroovyMock(Project, global: true)
    }

    def 'delegates an add offer call to the domain with proper params'() {
        given:
            Map params = [projId: projectId, name: offerName]

        when:
            Offer returnedOffer = facade.add(params)

        then:
            1 * Project.addOffer(projectId, _) >> { projId, offer -> offer }
            returnedOffer.name == params.name

        where:
            projectId | offerName
            1         | 'an Offer'
            15        | 'whasup!?'
            123       | 'doskonała oferta - kup teraz!'
    }
}
So we test a facade responsible for handling "add offer to the project" call triggered  somewhere in a GUI.
We want to ensure that static method Project.addOffer(long, Offer) will receive correct params when java.util.Map with user form input comes to the facade.add(params).
This is unit test, so how Project.addOffer() works is out of scope. Thus we want to stub it.

The most important is a GroovyMock(Project, global: true) statement.
What it does is modifing Project class to behave like a Spock's mock. 
GroovyMock() itself is a method inherited from SpecificationThe global flag is necessary to enable mocking static methods.
However when one comes to the need of mocking static method, author of Spock Framework advice to consider redesigning of implementation. It's not a bad advice, I must say.

Another important thing are assertions at then: block. First one checks an interaction, if the Project.addOffer() method was called exactly once, with a 1st argument equal to the projectId and some other param (we don't have an object instance yet to assert anything about it).
Right shit operator leads us to the stub which replaces original method implementation by such statement.
As a good stub it does nothing. The original method definition has return type Offer. The stub needs to do the same. So an offer passed as the 2nd argument is just returned.
Thanks to this we can assert about name property if it's equal with the value from params. If no return was designed the name could be checked inside the stub Closure, prefixed with an assert keyword.

Worth of  mentioning is that if you want to track interactions of original static method implementation without replacing it, then you should try using GroovySpy instead of GroovyMock.

Unfortunately static methods declared at Java object can't be treated in such ways. Though regular mocks and whole goodness of Spock can be used to test pure Java code, which is awesome anyway :)