Serielle Bearbeitung von APEX-Sessions erzwingen

von Peter Raganitsch, click-click IT Solutions e.U., Wien, mailto:Peter[dot]Raganitsch[at]click[minus]click[dot]at

Oracle APEX-Anwendungen sind echte Web-Anwendungen: sie haben keine dedizierte Datenbankverbindung (wie z.B. Oracle Forms) und verwenden das normale HTTP-Protokoll. Der Client schickt also eine Anfrage an den Webserver und wartet dann auf auf eine (HTML)-Antwort, die vom Browser dargestellt wird. Dabei ist es dem Browser egal, ob er die vorherige Antwort schon bekommen hat. Wenn der User einen Link oder Button klickt, wird die entsprechende Anfrage an den Webserver gestellt.

Nun kann es sein, dass der Webserver noch mit der vorherigen Anfrage beschäftigt ist, wenn die neue Anfrage vom User kommt. Dann wird die neue Anfrage einfach in einem weiteren Thread parallel zur vorherigen Anfrage bearbeitet.

Was im Falle von einfachen HTML-Dateien die vom Server angefordert werden kein Problem ist, wird bei APEX Anwendungen mitunter problematisch. Der Webserver erstellt hier bei der Bearbeitung einer Anfrage eine Datenbankverbindung und führt dort die APEX-Funktion "f" aus. Diese Funktion kennen sie sicher alle aus der Adresszeile von APEX Anwendungen.

Solange von der Datenbank gelesen wird, ist das auch alles kein Problem, im schlimmsten Fall werden hier Daten gelesen, die keiner mehr empfangen will. Bei schreibenden Operationen kann es hier aber zu Lock-Konflikten, falschen oder überschriebenen Daten kommen.

Folgendes Beispiel-Szenario:

  • Beim Aufruf einer Seite wird im Before-Header-Process eine Collection aufgebaut, die von einem Report auf der gleichen Seite verwendet und auch von den Folgeseiten gelesen wird.
  • Nun kann es vorkommen, dass der Aufbau der Collection mehrere Sekunden dauert
  • Ist der Benutzer ungeduldig und klickt mehrmals auf den Menü-Link, der unsere Collection-Seite aufruft, dann werden neue Anfragen an den Server gestellt, bevor die alte abgearbeitet ist
  • Das führt dazu, dass unser Before-Header-Process parallel mehrmals (1x pro User-Klick) in der Datenbank ausgeführt werden
  • nun legt die erste Session die Collection gerade an, während die zweite Session startet
  • die zweite Session sieht die Collection allerdings noch nicht und versucht diese anzulegen
  • währenddessen wird die erste Session fertig, committed die Anlage der Collection und versucht die Antwort an den User zu retournieren, der allerdings inzwischen nicht mehr darauf, sondern auf die Antwort von Session 2 wartet
  • Session 2 ist mit dem Aufbau der Collection fertig, löst ein commit aus und bekommt einen Fehler: Collection already exists

Dieses Verhalten ist für den Benutzer ziemlich frustrierend, da er zuerst lange warten muss und dann noch eine Fehlermeldung angezeigt bekommt.

Abhilfe kann hier ein erzwungenes Serialisieren der Ausführungen der Prozesse schaffen. Dazu wird vor dem Erstellen der Collection die Prozedur ApexLib_Serialize.serializeSessions aufgerufen, die mithilfe der SESSION-ID und dem Package DBMS_LOCK die Ausführung der Prozesse in der Datenbank serialisiert, d.h. sie werden nicht neben-, sondern hintereinander ausgeführt.

Das lässt den Benutzer u.U. länger warten, allerdings bekommt er sicher eine saubere Collection und damit die Daten die er sehen will.

Der Code dazu:

CREATE OR REPLACE PACKAGE ApexLib_Serialize IS
--******************************************************************************
--
--  PROJECT:  Apex Library
--
--  FILE:     ApexLib_Serialize.pks
--
--  DESCRIPTION:
--
--    This package holds routines to serialize APEX Sessions.
--
--    Requires DBMS_LOCK !
--
--  AUTHORS:
--
--    PR: Peter Raganitsch (http://www.oracle-and-apex.com/)
--
--******************************************************************************
--
--==============================================================================
-- Serializes multiple parallel requests from one APEX Session.
-- This can happen, when user clicks a link multiple times before apex can
-- answer his request. in this case multiple parallel database sessions are
-- running and executing the code.
-- In most cases this is not a problem, but sometimes you need to ensure that
-- all these requests are executed one after the other.
--
-- To achieve this simply call this procedure, it uses DBMS_LOCK to serialize
-- those parallel running sessions.
--==============================================================================
PROCEDURE serializeSessions;
--
--
END ApexLib_Serialize;
--==============================================================================
/
CREATE OR REPLACE PACKAGE BODY ApexLib_Serialize IS
--******************************************************************************
--
--  PROJECT:  Apex Library
--
--  FILE:     ApexLib_Serialize.pkb
--
--  DESCRIPTION:
--
--    This package holds routines to serialize APEX Sessions.
--
--    Requires DBMS_LOCK !
--
--  AUTHORS:
--
--    PR: Peter Raganitsch (http://www.oracle-and-apex.com/)
--
--***************************************************************************
--
--
MODULE_NAME     CONSTANT VARCHAR2(30) := 'ApexLib_Serialize';
gLockHandle              VARCHAR2(200);
--
--==============================================================================
-- Serializes multiple parallel requests from one APEX Session.
-- This can happen, when user clicks a link multiple times before apex can
-- answer his request. in this case multiple parallel database sessions are
-- running and executing the code.
-- In most cases this is not a problem, but sometimes you need to ensure that
-- all these requests are executed one after the other.
-- To achieve this simply call this procedure, it uses DBMS_LOCK to serialize
-- those parallel running sessions.
--==============================================================================
PROCEDURE serializeSessions
IS
    vStatus     INTEGER;
BEGIN
    -- request the lock only if we don't have the handle already
    -- (requesting again would release it at first and an other waiting
    --  session would get it).
    IF gLockHandle IS NULL
    THEN
        DBMS_LOCK.Allocate_Unique
          ( lockname          => v('APP_SESSION')
          , lockhandle        => gLockHandle
          );
        --
        vStatus := DBMS_LOCK.Request
          ( lockhandle        => gLockHandle
          , lockmode          => DBMS_LOCK.X_MODE
          , timeout           => 300 -- 5min wait
          , release_on_commit => TRUE
          );
    END IF;
    --
END serializeSessions;
--
END ApexLib_Serialize;
--
--==============================================================================
/

Im Page-Process sieht es dann in etwa so aus:

BEGIN
    -- Sicherstellen, dass auch bei mehrmaligem Aufruf der Seite alles nach der Reihe
    -- abgearbeitet wird. Parallele Bearbeitung würde die Collections zerstören.
    ApexLib_Serialize.serializeSessions;
    --
    IF APEX_COLLECTION.COLLECON_EXISTS('POOL_DATA')
    THEN
        APEX_COLLECTION.DELETE_COLLECTION('POOL_DATA');
    END IF;
    --
    APEX_COLLECTION.CREATE_COLLECTION_FROM_QUERY
      ( p_collection_name => 'POOL_DATA'
      , p_query           => 
          'SELECT SERIAL_NUMBER '||
          '    , USERNAME '||
          '    , POOL_TYPE '||
          '    , CONTACT_INFO '||
          '    , DIAMETER_MAX '||
          '    , DIAMETER_AVG '||
          '    , DIAMETER_MIN '||
          '    , PERSON_OID '||
          ' FROM CLZ_V_POOL_DATA '||
      );
END;

Das Ergebnis ist wie erwartet, egal wie oft der Benutzer die Seite anfordert, es kommt zu keinen Überschneidungen in der Bearbeitung und zu keiner Fehlermeldung. Alle anderen Benutzer können übrigens ungestört weiterarbeiten, da dieser Mechanismus sich auf die SESSION-ID stützt.

Was tun, wenn kein Zugriff auf DMBS_LOCK besteht?

Anstelle des DBMS_LOCK Packages kann hier natürlich auch mit einer sogenannten MUTEX-Tabelle gearbeitet werden. Dazu wird eine Tabelle erstellt, in die die fragliche APEX-Session-ID eingetragen und beim Serialisieren mit SELECT FOR UPDATE WAIT xxxxx; angefordert wird.

Selbstverständlich gibt es auch andere Methoden, um mehrfache Klicks auf denselben Link/Button/Menüeintrag auszuschliessen (verstecken, ausgrauen, disablen,...), allerdings kann am Client jedweder Check ausgehebelt oder übergangen werden, deswegen ist auch immer die serverseitige Überprüfung nötig.

Zurück zur Community-Seite